├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
├── Feature.php
├── Rollout.php
├── RolloutUserInterface.php
└── Storage
│ ├── ArrayStorage.php
│ ├── DoctrineCacheStorageAdapter.php
│ ├── MongoDBStorageAdapter.php
│ ├── PDOStorageAdapter.php
│ ├── RedisStorageAdapter.php
│ └── StorageInterface.php
└── tests
├── FeatureTest.php
├── RolloutTest.php
└── Storage
├── MongoDBStorageAdapterTest.php
├── PDOStorageAdapterTest.php
└── RedisStorageAdapterTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | phpunit.xml
3 | /vendor/
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | services:
3 | - mongodb
4 | php:
5 | - 5.6
6 | - 7.0
7 | - 7.1
8 | - 7.2
9 | - nightly
10 | - hhvm
11 |
12 | matrix:
13 | fast_finish: true
14 | allow_failures:
15 | - php: nightly
16 | - php: hhvm
17 | before_script: echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
18 | install: composer install -n
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 2.0.0
2 | -----
3 |
4 | (BC Break) The `StorageInterface` interface had a `remove` function added to its definition. If you have your own implementation of this interface, you'll need to update your implementation to include this functionality.
5 |
6 | - Added support for removing a feature
7 |
8 | 1.0.4
9 | -----
10 |
11 | - Added redis support [PR #9](https://github.com/opensoft/rollout/pull/9)
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 James Golick
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | rollout (for php)
2 | =================
3 |
4 | [](https://travis-ci.org/opensoft/rollout) [](https://scrutinizer-ci.com/g/opensoft/rollout/) [](https://scrutinizer-ci.com/g/opensoft/rollout/)
5 |
6 | Feature flippers for PHP. A port of ruby's [rollout](https://github.com/FetLife/rollout).
7 |
8 | Install It
9 | ----------
10 |
11 | composer require opensoft/rollout
12 |
13 | How it works
14 | ------------
15 |
16 | Initialize a rollout object:
17 |
18 | ```php
19 | use Opensoft\Rollout\Rollout;
20 | use Opensoft\Rollout\Storage\ArrayStorage;
21 |
22 | $rollout = new Rollout(new ArrayStorage());
23 | ```
24 |
25 | Check if a feature is active for a particular user:
26 |
27 | ```php
28 | $rollout->isActive('chat', $user); // returns true/false
29 | ```
30 |
31 | Check if a feature is activated globally:
32 |
33 | ```php
34 | $rollout->isActive('chat'); // returns true/false
35 | ```
36 |
37 | Storage
38 | -------
39 |
40 | There are a number of different storage implementations for where the configuration for the rollout is stored.
41 |
42 | * ArrayStorage - default storage, not persistent
43 | * DoctrineCacheStorageAdapter - requires [doctrine/cache][doctrine-cache]
44 | * PDOStorageAdapter - persistent using [PDO][pdo]
45 | * RedisStorageAdapter - persistent using [Redis][redis]
46 | * MongoDBStorageAdapter - persistent using [Mongo][mongo]
47 |
48 | [doctrine-cache]: https://packagist.org/packages/doctrine/cache
49 | [pdo]: http://php.net/pdo
50 | [redis]: http://redis.io
51 | [mongo]: http://mongodb.org
52 |
53 | All storage adapters must implement `Opensoft\Rollout\Storage\StorageInterface`.
54 |
55 | Groups
56 | ------
57 |
58 | Rollout ships with one group by default: `all`, which does exactly what it sounds like.
59 |
60 | You can activate the `all` group for chat features like this:
61 |
62 | ```php
63 | $rollout->activateGroup('chat', 'all');
64 | ```
65 |
66 | You may also want to define your own groups. We have one for caretakers:
67 |
68 | ```php
69 | $rollout->defineGroup('caretakers', function(RolloutUserInterface $user = null) {
70 | if (null === $user) {
71 | return false;
72 | }
73 |
74 | return $user->isCaretaker(); // boolean
75 | });
76 | ```
77 |
78 | You can activate multiple groups per feature.
79 |
80 | Deactivate groups like this:
81 |
82 | ```php
83 | $rollout->deactivateGroup('chat');
84 | ```
85 |
86 | Specific Users
87 | --------------
88 |
89 | 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:
90 |
91 | ```php
92 | $rollout->activateUser('chat', $user);
93 | ```
94 |
95 | Deactivate them like this:
96 |
97 | ```php
98 | $rollout->deactivateUser('chat', $user);
99 | ```
100 |
101 | Rollout users must implement the `RolloutUserInterface`.
102 |
103 | User Percentages
104 | ----------------
105 |
106 | 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.
107 |
108 | ```php
109 | $rollout->activatePercentage('chat', 20);
110 | ```
111 |
112 | The algorithm for determining which users get let in is this:
113 |
114 | ```php
115 | crc32($user->getRolloutIdentifier()) % 100 < $percentage
116 | ```
117 |
118 | So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users would remain in as the percentage increases.
119 |
120 | Deactivate all percentages like this:
121 |
122 | ```php
123 | $rollout->deactivatePercentage('chat');
124 | ```
125 |
126 | **Note:** Activating a feature for 100% of users will also make it activate `globally`. This is like calling `$rollout->isActive()` without a user object.
127 |
128 | Feature is Broken
129 | -----------------
130 |
131 | Deactivate everybody at once:
132 |
133 | ```php
134 | $rollout->deactivate('chat');
135 | ```
136 |
137 | You may wish to disable features programmatically if monitoring tools detect unusually high error rates for example.
138 |
139 | Remove a Feature (added in 2.0.0)
140 | ---------------------------------
141 |
142 | After a feature becomes mainstream or a failed experiment, you may want to remove the feature definition from rollout.
143 |
144 | ```php
145 | $rollout->remove('chat');
146 | ```
147 |
148 | Note: If there is still code referencing the feature, it will be recreated with default settings.
149 |
150 | Symfony2 Bundle
151 | ---------------
152 |
153 | A Symfony2 bundle is available to integrate rollout into Symfony2 projects. It can be found at http://github.com/opensoft/OpensoftRolloutBundle.
154 |
155 | Zend Framework 2 Module
156 | -----------------------
157 |
158 | A Zend Framework 2 module is availabile to intergrate rollout into Zend Framwork 2 projects. It can be found at https://github.com/adlogix/zf2-opensoft-rollout.
159 |
160 | Implementations in other languages
161 | ----------------------------------
162 |
163 | * Ruby: http://github.com/FetLife/rollout
164 | * Python: http://github.com/asenchi/proclaim
165 |
166 | Copyright
167 | ---------
168 |
169 | Copyright © 2017 James Golick, BitLove, Inc. See LICENSE for details.
170 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opensoft/rollout",
3 | "description": "Feature switches or flags for PHP",
4 | "keywords": ["feature", "flag", "toggle", "rollout", "flipper", "switches"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Richard Fullmer",
9 | "email": "richard.fullmer@opensoftdev.com"
10 | }
11 | ],
12 | "require": {
13 | "php": ">=5.3.3"
14 | },
15 | "suggest": {
16 | "doctrine/cache": "For use with the DoctrineCacheStorageAdapter",
17 | "predis/predis": "For use with the RedisStorageAdapter"
18 | },
19 | "require-dev": {
20 | "doctrine/cache": "*",
21 | "phpunit/phpunit": ">=4.0,<6.0"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "Opensoft\\Rollout\\": "src/"
26 | }
27 | },
28 | "minimum-stability": "dev"
29 | }
30 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 | tests
19 |
20 |
21 |
22 |
23 |
24 | src/
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Feature.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class Feature
12 | {
13 | /**
14 | * @var string
15 | */
16 | private $name;
17 |
18 | /**
19 | * @var array
20 | */
21 | private $groups = array();
22 |
23 | /**
24 | * @var array
25 | */
26 | private $users = array();
27 |
28 | /**
29 | * @var integer
30 | */
31 | private $percentage = 0;
32 |
33 | /**
34 | * @var string|null
35 | */
36 | private $requestParam;
37 |
38 | /**
39 | * @var array
40 | */
41 | private $data = array();
42 |
43 | /**
44 | * @param string $name
45 | * @param string|null $settings
46 | */
47 | public function __construct($name, $settings = null)
48 | {
49 | $this->name = $name;
50 | if ($settings) {
51 | $settings = explode('|', $settings);
52 |
53 | if (isset($settings[3])) {
54 | $rawRequestParam = $settings[3];
55 | $this->requestParam = $rawRequestParam;
56 | }
57 |
58 | //We can not trust the list function because of backwords compatibility
59 | if (isset($settings[4])) {
60 | $rawData = $settings[4];
61 | $this->data = !empty($rawData)? json_decode($rawData, true) : array();
62 | }
63 |
64 | list($rawPercentage, $rawUsers, $rawGroups) = $settings;
65 | $this->percentage = (int) $rawPercentage;
66 | $this->users = !empty($rawUsers) ? explode(',', $rawUsers) : array();
67 | $this->groups = !empty($rawGroups) ? explode(',', $rawGroups) : array();
68 | } else {
69 | $this->clear();
70 | }
71 | }
72 |
73 | /**
74 | * @return string
75 | */
76 | public function getName()
77 | {
78 | return $this->name;
79 | }
80 |
81 | /**
82 | * @param integer $percentage
83 | */
84 | public function setPercentage($percentage)
85 | {
86 | $this->percentage = $percentage;
87 | }
88 |
89 | /**
90 | * @return integer
91 | */
92 | public function getPercentage()
93 | {
94 | return $this->percentage;
95 | }
96 |
97 | /**
98 | * @return string
99 | */
100 | public function serialize()
101 | {
102 | return implode('|', array(
103 | $this->percentage,
104 | implode(',', $this->users),
105 | implode(',', $this->groups),
106 | $this->requestParam,
107 | json_encode($this->data)
108 | ));
109 | }
110 |
111 | /**
112 | * @param RolloutUserInterface $user
113 | */
114 | public function addUser(RolloutUserInterface $user)
115 | {
116 | if (!in_array($user->getRolloutIdentifier(), $this->users)) {
117 | $this->users[] = $user->getRolloutIdentifier();
118 | }
119 | }
120 |
121 | /**
122 | * @param RolloutUserInterface $user
123 | */
124 | public function removeUser(RolloutUserInterface $user)
125 | {
126 | if (($key = array_search($user->getRolloutIdentifier(), $this->users)) !== false) {
127 | unset($this->users[$key]);
128 | }
129 | }
130 |
131 | /**
132 | * @return array
133 | */
134 | public function getUsers()
135 | {
136 | return $this->users;
137 | }
138 |
139 | /**
140 | * @param string $group
141 | */
142 | public function addGroup($group)
143 | {
144 | if (!in_array($group, $this->groups)) {
145 | $this->groups[] = $group;
146 | }
147 | }
148 |
149 | /**
150 | * @param string $group
151 | */
152 | public function removeGroup($group)
153 | {
154 | if (($key = array_search($group, $this->groups)) !== false) {
155 | unset($this->groups[$key]);
156 | }
157 | }
158 |
159 | /**
160 | * @return array
161 | */
162 | public function getGroups()
163 | {
164 | return $this->groups;
165 | }
166 |
167 | /**
168 | * @return string|null
169 | */
170 | public function getRequestParam()
171 | {
172 | return $this->requestParam;
173 | }
174 |
175 | /**
176 | * @param string|null $requestParam
177 | */
178 | public function setRequestParam($requestParam)
179 | {
180 | $this->requestParam = $requestParam;
181 | }
182 |
183 | /**
184 | * @return array
185 | */
186 | public function getData()
187 | {
188 | return $this->data;
189 | }
190 |
191 | /**
192 | * @param array $data
193 | */
194 | public function setData(array $data)
195 | {
196 | $this->data = $data;
197 | }
198 |
199 | /**
200 | * Clear the feature of all configuration
201 | */
202 | public function clear()
203 | {
204 | $this->groups = array();
205 | $this->users = array();
206 | $this->percentage = 0;
207 | $this->requestParam = '';
208 | $this->data = array();
209 | }
210 |
211 | /**
212 | * Is the feature active?
213 | *
214 | * @param Rollout $rollout
215 | * @param RolloutUserInterface|null $user
216 | * @param array $requestParameters
217 | * @return bool
218 | */
219 | public function isActive(Rollout $rollout, RolloutUserInterface $user = null, array $requestParameters = array())
220 | {
221 | if (null == $user) {
222 | return $this->isParamInRequestParams($requestParameters)
223 | || $this->percentage == 100
224 | || $this->isInActiveGroup($rollout);
225 | }
226 |
227 | return $this->isParamInRequestParams($requestParameters) ||
228 | $this->isUserInPercentage($user) ||
229 | $this->isUserInActiveUsers($user) ||
230 | $this->isInActiveGroup($rollout, $user);
231 | }
232 |
233 | /**
234 | * @return array
235 | */
236 | public function toArray()
237 | {
238 | return array(
239 | 'percentage' => $this->percentage,
240 | 'groups' => $this->groups,
241 | 'users' => $this->users,
242 | 'requestParam' => $this->requestParam,
243 | 'data'=> $this->data
244 | );
245 | }
246 |
247 | /**
248 | * @param array $requestParameters
249 | * @return bool
250 | */
251 | private function isParamInRequestParams(array $requestParameters)
252 | {
253 | $param = explode('=', $this->requestParam);
254 | $key = array_shift($param);
255 | $value = array_shift($param);
256 |
257 | return $key && array_key_exists($key, $requestParameters) &&
258 | (empty($value) || $requestParameters[$key] == $value);
259 | }
260 |
261 | /**
262 | * @param RolloutUserInterface $user
263 | * @return bool
264 | */
265 | private function isUserInPercentage(RolloutUserInterface $user)
266 | {
267 | return abs(crc32($user->getRolloutIdentifier()) % 100) < $this->percentage;
268 | }
269 |
270 | /**
271 | * @param RolloutUserInterface $user
272 | * @return boolean
273 | */
274 | private function isUserInActiveUsers(RolloutUserInterface $user)
275 | {
276 | return in_array($user->getRolloutIdentifier(), $this->users);
277 | }
278 |
279 | /**
280 | * @param Rollout $rollout
281 | * @param RolloutUserInterface|null $user
282 | * @return bool
283 | */
284 | private function isInActiveGroup(Rollout $rollout, RolloutUserInterface $user = null)
285 | {
286 | foreach ($this->groups as $group) {
287 | if ($rollout->isActiveInGroup($group, $user)) {
288 | return true;
289 | }
290 | }
291 |
292 | return false;
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/src/Rollout.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class Rollout
14 | {
15 | /**
16 | * @var StorageInterface
17 | */
18 | private $storage;
19 |
20 | /**
21 | * @var array
22 | */
23 | private $groups;
24 |
25 | /**
26 | * @param StorageInterface $storage
27 | */
28 | public function __construct(StorageInterface $storage)
29 | {
30 | $this->storage = $storage;
31 | $this->groups = array(
32 | 'all' => function(RolloutUserInterface $user) { return $user !== null; }
33 | );
34 | }
35 |
36 | /**
37 | * @param string $feature
38 | */
39 | public function activate($feature)
40 | {
41 | $feature = $this->get($feature);
42 | if ($feature) {
43 | $feature->setPercentage(100);
44 | $this->save($feature);
45 | }
46 | }
47 |
48 | /**
49 | * @param string $feature
50 | */
51 | public function deactivate($feature)
52 | {
53 | $feature = $this->get($feature);
54 | if ($feature) {
55 | $feature->clear();
56 | $this->save($feature);
57 | }
58 | }
59 |
60 | /**
61 | * @param string $feature
62 | * @param string $group
63 | */
64 | public function activateGroup($feature, $group)
65 | {
66 | $feature = $this->get($feature);
67 | if ($feature) {
68 | $feature->addGroup($group);
69 | $this->save($feature);
70 | }
71 | }
72 |
73 | /**
74 | * @param string $feature
75 | * @param string $group
76 | */
77 | public function deactivateGroup($feature, $group)
78 | {
79 | $feature = $this->get($feature);
80 | if ($feature) {
81 | $feature->removeGroup($group);
82 | $this->save($feature);
83 | }
84 | }
85 |
86 | /**
87 | * @param string $feature
88 | * @param RolloutUserInterface $user
89 | */
90 | public function activateUser($feature, RolloutUserInterface $user)
91 | {
92 | $feature = $this->get($feature);
93 | if ($feature) {
94 | $feature->addUser($user);
95 | $this->save($feature);
96 | }
97 | }
98 |
99 | /**
100 | * @param string $feature
101 | * @param RolloutUserInterface $user
102 | */
103 | public function deactivateUser($feature, RolloutUserInterface $user)
104 | {
105 | $feature = $this->get($feature);
106 | if ($feature) {
107 | $feature->removeUser($user);
108 | $this->save($feature);
109 | }
110 | }
111 |
112 | /**
113 | * @param string $group
114 | * @param \Closure $closure
115 | */
116 | public function defineGroup($group, \Closure $closure)
117 | {
118 | $this->groups[$group] = $closure;
119 | }
120 |
121 | /**
122 | * @param string $feature
123 | * @param RolloutUserInterface|null $user
124 | * @param array $requestParameters
125 | * @return bool
126 | */
127 | public function isActive($feature, RolloutUserInterface $user = null, array $requestParameters = array())
128 | {
129 | $feature = $this->get($feature);
130 |
131 | return $feature ? $feature->isActive($this, $user, $requestParameters) : false;
132 | }
133 |
134 | /**
135 | * @param string $feature
136 | * @param integer $percentage
137 | */
138 | public function activatePercentage($feature, $percentage)
139 | {
140 | $feature = $this->get($feature);
141 | if ($feature) {
142 | $feature->setPercentage($percentage);
143 | $this->save($feature);
144 | }
145 | }
146 |
147 | /**
148 | * @param string $feature
149 | */
150 | public function deactivatePercentage($feature)
151 | {
152 | $feature = $this->get($feature);
153 | if ($feature) {
154 | $feature->setPercentage(0);
155 | $this->save($feature);
156 | }
157 | }
158 |
159 | /**
160 | * @param string $feature
161 | * @param string $requestParam
162 | */
163 | public function activateRequestParam($feature, $requestParam)
164 | {
165 | $feature = $this->get($feature);
166 | if ($feature) {
167 | $feature->setRequestParam($requestParam);
168 | $this->save($feature);
169 | }
170 | }
171 |
172 | /**
173 | * @param string $feature
174 | */
175 | public function deactivateRequestParam($feature)
176 | {
177 | $feature = $this->get($feature);
178 | if ($feature) {
179 | $feature->setRequestParam('');
180 | $this->save($feature);
181 | }
182 | }
183 |
184 | /**
185 | * @param string $group
186 | * @param RolloutUserInterface|null $user
187 | * @return bool
188 | */
189 | public function isActiveInGroup($group, RolloutUserInterface $user = null)
190 | {
191 | if (!isset($this->groups[$group])) {
192 | return false;
193 | }
194 |
195 | $g = $this->groups[$group];
196 |
197 | return $g && $g($user);
198 | }
199 |
200 | /**
201 | * @param string $feature
202 | * @return Feature
203 | */
204 | public function get($feature)
205 | {
206 | $settings = $this->storage->get($this->key($feature));
207 |
208 | if (!empty($settings)) {
209 | $f = new Feature($feature, $settings);
210 | } else {
211 | $f = new Feature($feature);
212 |
213 | $this->save($f);
214 | }
215 |
216 | return $f;
217 | }
218 |
219 | /**
220 | * Remove a feature definition from rollout
221 | *
222 | * @param string $feature
223 | */
224 | public function remove($feature)
225 | {
226 | $this->storage->remove($this->key($feature));
227 |
228 | $features = $this->features();
229 | if (in_array($feature, $features)) {
230 | $features = array_diff($features, array($feature));
231 | }
232 | $this->storage->set($this->featuresKey(), implode(',', $features));
233 | }
234 |
235 | /**
236 | * Update feature specific data
237 | *
238 | * @example $rollout->setFeatureData('chat', array(
239 | * 'description' => 'foo',
240 | * 'release_date' => 'bar',
241 | * 'whatever' => 'baz'
242 | * ));
243 | *
244 | * @param string $feature
245 | * @param array $data
246 | */
247 | public function setFeatureData($feature, array $data)
248 | {
249 | $feature = $this->get($feature);
250 | if ($feature) {
251 | $feature->setData(array_merge($feature->getData(), $data));
252 | $this->save($feature);
253 | }
254 | }
255 |
256 | /**
257 | * Clear all feature data
258 | *
259 | * @param string $feature
260 | */
261 | public function clearFeatureData($feature)
262 | {
263 | $feature = $this->get($feature);
264 | if ($feature) {
265 | $feature->setData(array());
266 | $this->save($feature);
267 | }
268 | }
269 |
270 | /**
271 | * @return array
272 | */
273 | public function features()
274 | {
275 | $content = $this->storage->get($this->featuresKey());
276 |
277 | if (!empty($content)) {
278 | return explode(',', $content);
279 | }
280 |
281 | return array();
282 | }
283 |
284 | /**
285 | * @param string $name
286 | * @return string
287 | */
288 | private function key($name)
289 | {
290 | return 'feature:' . $name;
291 | }
292 |
293 | /**
294 | * @return string
295 | */
296 | private function featuresKey()
297 | {
298 | return 'feature:__features__';
299 | }
300 |
301 | /**
302 | * @param Feature $feature
303 | */
304 | protected function save(Feature $feature)
305 | {
306 | $name = $feature->getName();
307 | $this->storage->set($this->key($name), $feature->serialize());
308 |
309 | $features = $this->features();
310 | if (!in_array($name, $features)) {
311 | $features[] = $name;
312 | }
313 | $this->storage->set($this->featuresKey(), implode(',', $features));
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/RolloutUserInterface.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | interface RolloutUserInterface
13 | {
14 | /**
15 | * @return string
16 | */
17 | public function getRolloutIdentifier();
18 | }
19 |
--------------------------------------------------------------------------------
/src/Storage/ArrayStorage.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class ArrayStorage implements StorageInterface
12 | {
13 | /**
14 | * @var array
15 | */
16 | private $storage = array();
17 |
18 | /**
19 | * @param string $key
20 | * @return mixed|null
21 | */
22 | public function get($key)
23 | {
24 | return isset($this->storage[$key]) ? $this->storage[$key] : null;
25 | }
26 |
27 | /**
28 | * @param string $key
29 | * @param mixed $value
30 | */
31 | public function set($key, $value)
32 | {
33 | $this->storage[$key] = $value;
34 | }
35 |
36 | /**
37 | * @param string $key
38 | */
39 | public function remove($key)
40 | {
41 | if (isset($this->storage[$key])) {
42 | unset($this->storage[$key]);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Storage/DoctrineCacheStorageAdapter.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class DoctrineCacheStorageAdapter implements StorageInterface
16 | {
17 | /**
18 | * @var Cache
19 | */
20 | private $cache;
21 |
22 | /**
23 | * @param Cache $cache
24 | */
25 | public function __construct(Cache $cache)
26 | {
27 | $this->cache = $cache;
28 | }
29 |
30 | /**
31 | * @param string $key
32 | * @return mixed|null Null if the value is not found
33 | */
34 | public function get($key)
35 | {
36 | return $this->cache->fetch($key);
37 | }
38 |
39 | /**
40 | * @param string $key
41 | * @param mixed $value
42 | */
43 | public function set($key, $value)
44 | {
45 | $this->cache->save($key, $value);
46 | }
47 |
48 | /**
49 | * @param string $key
50 | */
51 | public function remove($key)
52 | {
53 | $this->cache->delete($key);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Storage/MongoDBStorageAdapter.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class MongoDBStorageAdapter implements StorageInterface
11 | {
12 |
13 | /**
14 | * @var object
15 | */
16 | private $mongo;
17 |
18 | /**
19 | * @var string
20 | */
21 | private $collection;
22 |
23 | public function __construct($mongo, $collection = "rollout_feature")
24 | {
25 | $this->mongo = $mongo;
26 | $this->collection = $collection;
27 | }
28 | public function getCollectionName()
29 | {
30 | return $this->collection;
31 | }
32 | /**
33 | * @inheritdoc
34 | */
35 | public function get($key)
36 | {
37 | $collection = $this->getCollectionName();
38 | $result = $this->mongo->$collection->findOne(['name' => $key]);
39 |
40 | if (!$result) {
41 | return null;
42 | }
43 |
44 | return $result['value'];
45 | }
46 |
47 | /**
48 | * @inheritdoc
49 | */
50 | public function set($key, $value)
51 | {
52 | $collection = $this->getCollectionName();
53 | $this->mongo->$collection->update(['name' => $key], ['$set' => ['value' => $value]], ['upsert' => true]);
54 | }
55 |
56 | /**
57 | * @inheritdoc
58 | */
59 | public function remove($key)
60 | {
61 | $collection = $this->getCollectionName();
62 | $this->mongo->$collection->remove(['name' => $key]);
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/Storage/PDOStorageAdapter.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class PDOStorageAdapter implements StorageInterface
11 | {
12 | const STMT_SELECT = 'SELECT settings FROM :table WHERE name = :key';
13 | const STMT_INSERT = 'INSERT INTO :table (name, settings) VALUES (:key, :value)';
14 | const STMT_UPDATE = 'UPDATE :table SET settings = :value WHERE name = :key';
15 | const STMT_DELETE = 'DELETE FROM :table WHERE name = :key';
16 |
17 | /**
18 | * @var \PDO
19 | */
20 | private $pdoConnection;
21 |
22 | /**
23 | * @var string
24 | */
25 | private $tableName;
26 |
27 | public function __construct(\PDO $pdoConnection, $tableName = 'rollout_feature')
28 | {
29 | $this->pdoConnection = $pdoConnection;
30 | $this->tableName = $tableName;
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function get($key)
37 | {
38 | $statement = $this->pdoConnection->prepare($this->getSQLStatement(self::STMT_SELECT));
39 |
40 | $statement->bindParam('key', $key);
41 | $statement->setFetchMode(\PDO::FETCH_ASSOC);
42 |
43 | $statement->execute();
44 |
45 | $result = $statement->fetch();
46 |
47 | if (false === $result) {
48 | return null;
49 | }
50 |
51 | return $result['settings'];
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function set($key, $value)
58 | {
59 | if (null === $this->get($key)) {
60 | $sql = self::STMT_INSERT;
61 | } else {
62 | $sql = self::STMT_UPDATE;
63 | }
64 |
65 | $statement = $this->pdoConnection->prepare($this->getSQLStatement($sql));
66 |
67 | $statement->bindParam('key', $key);
68 | $statement->bindParam('value', $value);
69 |
70 | $statement->execute();
71 | }
72 |
73 | /**
74 | * @param string $key
75 | */
76 | public function remove($key)
77 | {
78 | $statement = $this->pdoConnection->prepare($this->getSQLStatement(self::STMT_DELETE));
79 |
80 | $statement->bindParam('key', $key);
81 | $statement->execute();
82 | }
83 |
84 | /**
85 | * @param string $sql
86 | *
87 | * @return string
88 | */
89 | private function getSQLStatement($sql)
90 | {
91 | return str_replace(':table', $this->tableName, $sql);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Storage/RedisStorageAdapter.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class RedisStorageAdapter implements StorageInterface
11 | {
12 | /**
13 | * @var string
14 | */
15 | const DEFAULT_GROUP = 'rollout_feature';
16 |
17 | /**
18 | * @var object
19 | */
20 | private $redis;
21 |
22 | /**
23 | * @var string
24 | */
25 | private $group = self::DEFAULT_GROUP;
26 |
27 | public function __construct($redis, $group = null)
28 | {
29 | $this->redis = $redis;
30 |
31 | if ($group) {
32 | $this->group = $group;
33 | }
34 | }
35 |
36 | /**
37 | * @inheritdoc
38 | */
39 | public function get($key)
40 | {
41 | $result = $this->redis->hget($this->group, $key);
42 |
43 | if (empty($result)) {
44 | return null;
45 | }
46 |
47 | $result = json_decode($result, true);
48 |
49 | if (JSON_ERROR_NONE !== json_last_error()) {
50 | return null;
51 | }
52 |
53 | return $result;
54 | }
55 |
56 | /**
57 | * @inheritdoc
58 | */
59 | public function set($key, $value)
60 | {
61 | $this->redis->hset($this->group, $key, json_encode($value));
62 | }
63 |
64 | /**
65 | * @inheritdoc
66 | */
67 | public function remove($key)
68 | {
69 | $this->redis->hdel($this->group, $key);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Storage/StorageInterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | interface StorageInterface
12 | {
13 | /**
14 | * @param string $key
15 | * @return string|null Null if the value is not found
16 | */
17 | public function get($key);
18 |
19 | /**
20 | * @param string $key
21 | * @param string $value
22 | * @return void
23 | */
24 | public function set($key, $value);
25 |
26 | /**
27 | * @param string $key
28 | * @return void
29 | */
30 | public function remove($key);
31 | }
32 |
--------------------------------------------------------------------------------
/tests/FeatureTest.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class FeatureTest extends \PHPUnit_Framework_TestCase
9 | {
10 | public function testParseOldSettingsFormat()
11 | {
12 | $feature = new Feature('chat', '100|4,12|fivesonly');
13 |
14 | $this->assertEquals(100, $feature->getPercentage());
15 | $this->assertEquals([4, 12], $feature->getUsers());
16 | $this->assertEquals(['fivesonly'], $feature->getGroups());
17 | }
18 |
19 | public function testParseNewSettingsFormat()
20 | {
21 | $feature = new Feature('chat', '100|4,12|fivesonly|FF_facebookIntegration=1');
22 |
23 | $this->assertEquals(100, $feature->getPercentage());
24 | $this->assertEquals([4, 12], $feature->getUsers());
25 | $this->assertEquals(['fivesonly'], $feature->getGroups());
26 | $this->assertEquals('FF_facebookIntegration=1', $feature->getRequestParam());
27 | }
28 |
29 | public function testParseDataSettingsFormat()
30 | {
31 | $feature = new Feature('chat', '100|4,12|fivesonly|FF_facebookIntegration=1|{"description":"foo","release_date":"bar"}');
32 |
33 | $this->assertEquals(100, $feature->getPercentage());
34 | $this->assertEquals([4, 12], $feature->getUsers());
35 | $this->assertEquals(['fivesonly'], $feature->getGroups());
36 | $this->assertEquals('FF_facebookIntegration=1', $feature->getRequestParam());
37 | $this->assertEquals('{"description":"foo","release_date":"bar"}', json_encode($feature->getData()));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/RolloutTest.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class RolloutTest extends \PHPUnit_Framework_TestCase
14 | {
15 | /**
16 | * @var Rollout
17 | */
18 | private $rollout;
19 |
20 | protected function setUp()
21 | {
22 | $this->rollout = new Rollout(new ArrayStorage());
23 | }
24 |
25 | public function testActiveForBlockGroup()
26 | {
27 | // When a group is activated
28 | $this->rollout->defineGroup('fivesonly', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() == 5; });
29 | $this->rollout->activateGroup('chat', 'fivesonly');
30 |
31 | // the feature is active for users for which the callback evaluates as true
32 | $this->assertTrue($this->rollout->isActive('chat', new RolloutUser(5)));
33 |
34 | // is not active for users for which the callback evalutates to false
35 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(1)));
36 |
37 | // is not active if a group is found in storage, but not defined in the rollout
38 | $this->rollout->activateGroup('chat', 'fake');
39 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(1)));
40 | }
41 |
42 | public function testDefaultAllGroup()
43 | {
44 | // the default all group
45 | $this->rollout->activateGroup('chat', 'all');
46 |
47 | // evaluates to true no matter what
48 | $this->assertTrue($this->rollout->isActive('chat', new RolloutUser(0)));
49 | }
50 |
51 | public function testDeactivatingAGroup()
52 | {
53 | $this->rollout->defineGroup('fivesonly', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() == 5; });
54 | $this->rollout->activateGroup('chat', 'all');
55 | $this->rollout->activateGroup('chat', 'some');
56 | $this->rollout->activateGroup('chat', 'fivesonly');
57 | $this->rollout->deactivateGroup('chat', 'all');
58 | $this->rollout->deactivateGroup('chat', 'some');
59 |
60 | // deactivates the rules for that group
61 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(10)));
62 |
63 | // leaves the other groups active
64 | $this->assertContains('fivesonly', $this->rollout->get('chat')->getGroups());
65 | $this->assertCount(1, $this->rollout->get('chat')->getGroups());
66 | }
67 |
68 | public function testDeactivatingAFeatureCompletely()
69 | {
70 | $this->rollout->defineGroup('fivesonly', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() === 5; });
71 | $this->rollout->activateGroup('chat', 'all');
72 | $this->rollout->activateGroup('chat', 'fivesonly');
73 | $this->rollout->activateUser('chat', new RolloutUser(51));
74 | $this->rollout->activatePercentage('chat', 100);
75 | $this->rollout->activateRequestParam('chat', 'FF_facebookIntegration=1');
76 | $this->rollout->activate('chat');
77 | $this->rollout->deactivate('chat');
78 |
79 | // it should remove all of the groups
80 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(0)));
81 |
82 | // it should remove all of the users
83 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(51)));
84 |
85 | // it should remove the percentage
86 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(24)));
87 |
88 | // it should remove the request param
89 | $this->assertFalse($this->rollout->isActive('chat', null, array('FF_facebookIntegration', true)));
90 |
91 | // it should be removed globally
92 | $this->assertFalse($this->rollout->isActive('chat'));
93 | }
94 |
95 | public function testActivatingASpecificUser()
96 | {
97 | $this->rollout->activateUser('chat', new RolloutUser(42));
98 |
99 | // it should be active for that user
100 | $this->assertTrue($this->rollout->isActive('chat', new RolloutUser(42)));
101 |
102 | // it remains inactive for other users
103 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(24)));
104 | }
105 |
106 | public function testActivatingASpecificUserWithStringId()
107 | {
108 | $this->rollout->activateUser('chat', new RolloutUser('user-72'));
109 |
110 | // it should be active for that user
111 | $this->assertTrue($this->rollout->isActive('chat', new RolloutUser('user-72')));
112 |
113 | // it remains inactive for other users
114 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser('user-12')));
115 | }
116 |
117 | public function testDeactivatingASpecificUser()
118 | {
119 | $this->rollout->activateUser('chat', new RolloutUser(42));
120 | $this->rollout->activateUser('chat', new RolloutUser(4242));
121 | $this->rollout->activateUser('chat', new RolloutUser(24));
122 | $this->rollout->deactivateUser('chat', new RolloutUser(42));
123 | $this->rollout->deactivateUser('chat', new RolloutUser('4242'));
124 |
125 | // that user should no longer be active
126 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(42)));
127 |
128 | // it remains active for other users
129 | $users = $this->rollout->get('chat')->getUsers();
130 | $this->assertCount(1, $users);
131 | $this->assertEquals(24, $users[0]);
132 | }
133 |
134 | public function testActivatingAFeatureGlobally()
135 | {
136 | $this->rollout->activate('chat');
137 |
138 | // it should activate the feature
139 | $this->assertTrue($this->rollout->isActive('chat'));
140 | }
141 |
142 | public function testActivatingAFeatureForPercentageOfUsers()
143 | {
144 | $this->rollout->activatePercentage('chat', 20);
145 |
146 | $activated = array();
147 | foreach (range(1, 120) as $id) {
148 | if ($this->rollout->isActive('chat', new RolloutUser($id))) {
149 | $activated[] = true;
150 | }
151 | }
152 |
153 | // it should activate the feature for a percentage of users
154 | $this->assertLessThanOrEqual(21, count($activated));
155 | $this->assertGreaterThanOrEqual(19, count($activated));
156 | }
157 |
158 | public function testActivatingAFeatureForPercentageOfUsers2()
159 | {
160 | $this->rollout->activatePercentage('chat', 20);
161 |
162 | $activated = array();
163 | foreach (range(1, 200) as $id) {
164 | if ($this->rollout->isActive('chat', new RolloutUser($id))) {
165 | $activated[] = true;
166 | }
167 | }
168 |
169 | // it should activate the feature for a percentage of users
170 | $this->assertLessThanOrEqual(45, count($activated));
171 | $this->assertGreaterThanOrEqual(35, count($activated));
172 | }
173 |
174 | public function testActivatingAFeatureForPercentageOfUsers3()
175 | {
176 | $this->rollout->activatePercentage('chat', 5);
177 |
178 | $activated = array();
179 | foreach (range(1, 100) as $id) {
180 | if ($this->rollout->isActive('chat', new RolloutUser($id))) {
181 | $activated[] = true;
182 | }
183 | }
184 |
185 | // it should activate the feature for a percentage of users
186 | $this->assertLessThanOrEqual(7, count($activated));
187 | $this->assertGreaterThanOrEqual(3, count($activated));
188 | }
189 |
190 | public function testActivatingAFeatureForAGroupAsAString()
191 | {
192 | $this->rollout->defineGroup('admins', function(RolloutUserInterface $user) { return $user->getRolloutIdentifier() == 5; });
193 | $this->rollout->activateGroup('chat', 'admins');
194 |
195 | // the feature is active for users for which the block is true
196 | $this->assertTrue($this->rollout->isActive('chat', new RolloutUser(5)));
197 |
198 | // the feature is not active for users for which the block evaluates to false
199 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(1)));
200 | }
201 |
202 | public function testDeactivatingThePercentageOfUsers()
203 | {
204 | $this->rollout->activatePercentage('chat', 100);
205 | $this->rollout->deactivatePercentage('chat');
206 |
207 | // it becomes inactive for all users
208 | $this->assertFalse($this->rollout->isActive('chat', new RolloutUser(24)));
209 | }
210 |
211 | public function testActivatingRequestParam()
212 | {
213 | $this->rollout->activateRequestParam('chat', 'FF_facebookIntegration=1');
214 |
215 | $this->assertTrue($this->rollout->isActive('chat', null, ['FF_facebookIntegration' => true]));
216 |
217 | $this->assertFalse($this->rollout->isActive('chat', null, ['FF_anotherFeature' => true]));
218 | }
219 |
220 | public function testDeactivatingRequestParam()
221 | {
222 | $this->rollout->activateRequestParam('chat', 'FF_facebookIntegration=1');
223 | $this->rollout->deactivateRequestParam('chat');
224 |
225 | $this->assertFalse($this->rollout->isActive('chat', null, ['FF_facebookIntegration' => true]));
226 |
227 | $this->assertFalse($this->rollout->isActive('chat', null, ['FF_anotherFeature' => true]));
228 | }
229 |
230 | public function testDeactivatingTheFeatureGlobally()
231 | {
232 | $this->rollout->activate('chat');
233 | $this->rollout->deactivate('chat');
234 |
235 | // inactive feature
236 | $this->assertFalse($this->rollout->isActive('chat'));
237 | }
238 |
239 | public function testKeepsAListOfFeatures()
240 | {
241 | // saves the feature
242 | $this->rollout->activate('chat');
243 | $this->assertContains('chat', $this->rollout->features());
244 |
245 | // does not contain doubles
246 | $this->rollout->activate('chat');
247 | $this->rollout->activate('chat');
248 | $this->assertCount(1, $this->rollout->features());
249 | }
250 |
251 | public function testGet()
252 | {
253 | $this->rollout->activatePercentage('chat', 10);
254 | $this->rollout->activateGroup('chat', 'caretakers');
255 | $this->rollout->activateGroup('chat', 'greeters');
256 | $this->rollout->activate('signup');
257 | $this->rollout->activateUser('chat', new RolloutUser(42));
258 | $this->rollout->activateRequestParam('chat', 'FF_facebookIntegration=1');
259 | $this->rollout->setFeatureData('chat', array(
260 | 'description' => 'foo',
261 | 'release_date' => 'bar',
262 | 'whatever' => 'baz'
263 | ));
264 |
265 | // it should return the feature object
266 | $feature = $this->rollout->get('chat');
267 | $this->assertContains('caretakers', $feature->getGroups());
268 | $this->assertContains('greeters', $feature->getGroups());
269 | $this->assertEquals(10, $feature->getPercentage());
270 | $this->assertContains(42, $feature->getUsers());
271 | $this->assertEquals(
272 | array(
273 | 'groups' => array('caretakers', 'greeters'),
274 | 'percentage' => 10,
275 | 'users' => array('42'),
276 | 'requestParam' => 'FF_facebookIntegration=1',
277 | 'data' => array(
278 | 'description' => 'foo',
279 | 'release_date' => 'bar',
280 | 'whatever' => 'baz'
281 | )
282 | ),
283 | $feature->toArray()
284 | );
285 |
286 | $feature = $this->rollout->get('signup');
287 | $this->assertEmpty($feature->getGroups());
288 | $this->assertEmpty($feature->getUsers());
289 | $this->assertEquals(100, $feature->getPercentage());
290 | $this->assertEmpty($feature->getRequestParam());
291 | $this->assertEmpty($feature->getData());
292 | }
293 |
294 | public function testRemove()
295 | {
296 | $this->rollout->activate('signup');
297 | $feature = $this->rollout->get('signup');
298 | $this->assertEquals('signup', $feature->getName());
299 |
300 | $this->rollout->remove('signup');
301 | $this->assertNotContains('signup', $this->rollout->features());
302 | }
303 |
304 | public function testClearFeatureData()
305 | {
306 | $this->rollout->activate('signup');
307 | $feature = $this->rollout->get('signup');
308 | $this->assertEquals('signup', $feature->getName());
309 |
310 | $this->rollout->setFeatureData('signup', array(
311 | 'description' => 'foo',
312 | 'release_date' => 'bar',
313 | 'whatever' => 'baz'
314 | ));
315 |
316 | $feature = $this->rollout->get('signup');
317 |
318 | $this->assertEquals(array(
319 | 'description' => 'foo',
320 | 'release_date' => 'bar',
321 | 'whatever' => 'baz'
322 | ), $feature->getData());
323 |
324 | $this->rollout->clearFeatureData('signup');
325 |
326 | $feature = $this->rollout->get('signup');
327 |
328 | $this->assertEmpty($feature->getData());
329 | }
330 | }
331 |
332 |
333 | /**
334 | * @author Richard Fullmer
335 | */
336 | class RolloutUser implements RolloutUserInterface
337 | {
338 | /**
339 | * @var string
340 | */
341 | private $id;
342 |
343 | /**
344 | * @param string $id
345 | */
346 | public function __construct($id)
347 | {
348 | $this->id = $id;
349 | }
350 |
351 | /**
352 | * @return string
353 | */
354 | public function getRolloutIdentifier()
355 | {
356 | return $this->id;
357 | }
358 | }
359 |
--------------------------------------------------------------------------------
/tests/Storage/MongoDBStorageAdapterTest.php:
--------------------------------------------------------------------------------
1 | mongo = new mockMongo();
17 | $collection = $this->mockCollection();
18 | $collection->method('findOne')->will($this->returnValue(['name' => 'key', 'value' => true]));
19 | $collection->method('update')->will($this->returnValue(null));
20 |
21 | $this->mongo->setCollection($collection);
22 | }
23 |
24 | public function testGet()
25 | {
26 |
27 | $adapter = new MongoDBStorageAdapter($this->mongo);
28 |
29 | $result = $adapter->get('key');
30 | $this->assertSame(true, $result);
31 | }
32 |
33 | public function testGetNoValue()
34 | {
35 |
36 | $adapter = new MongoDBStorageAdapter($this->mongo);
37 | $this->mongo->coll->method('findOne')->will($this->returnValue(null));
38 | $result = $adapter->get('key');
39 | $this->assertSame(true, $result);
40 | }
41 |
42 | public function testSet()
43 | {
44 |
45 | $adapter = new MongoDBStorageAdapter($this->mongo);
46 |
47 | $adapter->set('key', 'value');
48 |
49 | }
50 |
51 | public function testRemove()
52 | {
53 |
54 | $adapter = new MongoDBStorageAdapter($this->mongo);
55 |
56 | $adapter->remove('key');
57 |
58 | }
59 |
60 | public function testGetCollectionName()
61 | {
62 |
63 | $adapter = new MongoDBStorageAdapter($this->mongo, 'feature_test');
64 |
65 | $result = $adapter->getCollectionName();
66 | $this->assertSame('feature_test', $result);
67 |
68 | }
69 |
70 | public function mockCollection()
71 | {
72 | return $this->getMockBuilder('MongoCollection')
73 | ->disableOriginalConstructor()
74 | ->setMethods(array(
75 | 'find',
76 | 'findOne',
77 | 'update',
78 | 'remove',
79 | ))
80 | ->getMock();
81 |
82 | }
83 | }
84 |
85 | class mockMongo
86 | {
87 | public $collection;
88 | public function __construct()
89 | {
90 |
91 | }
92 |
93 | public function setCollection($mongoCollection)
94 | {
95 | $this->collection = $mongoCollection;
96 | }
97 |
98 | public function __get($name)
99 | {
100 | return $this->collection;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/tests/Storage/PDOStorageAdapterTest.php:
--------------------------------------------------------------------------------
1 | mockPDOStatement();
12 |
13 | $statement->expects($this->once())
14 | ->method('bindParam')
15 | ->with('key', 'test');
16 |
17 | $statement->expects($this->once())
18 | ->method('execute');
19 |
20 | $statement->expects($this->once())
21 | ->method('fetch')
22 | ->willReturn(array('settings' => 'success'));
23 |
24 | $pdo = $this->mockPDO($this->prepareSQL(PDOStorageAdapter::STMT_SELECT), $statement);
25 |
26 | $adapter = new PDOStorageAdapter($pdo);
27 |
28 | $result = $adapter->get('test');
29 | $this->assertEquals('success', $result);
30 | }
31 |
32 | public function testRemove()
33 | {
34 | $statement = $this->mockPDOStatement();
35 |
36 | $statement->expects($this->once())
37 | ->method('bindParam')
38 | ->with('key', 'test');
39 |
40 | $statement->expects($this->once())
41 | ->method('execute');
42 |
43 | $pdo = $this->mockPDO($this->prepareSQL(PDOStorageAdapter::STMT_DELETE), $statement);
44 |
45 | $adapter = new PDOStorageAdapter($pdo);
46 | $adapter->remove('test');
47 | }
48 |
49 | public function testTableName()
50 | {
51 | $statement = $this->mockPDOStatement();
52 |
53 | $pdo = $this->mockPDO($this->prepareSQL(PDOStorageAdapter::STMT_SELECT, 'rollout_feature2'), $statement);
54 |
55 | $adapter = new PDOStorageAdapter($pdo, 'rollout_feature2');
56 |
57 | $adapter->get('test');
58 | }
59 |
60 | public function testSetInsert()
61 | {
62 | $getStatement = $this->mockPDOStatement();
63 |
64 | $setStatement = $this->mockPDOStatement();
65 | $setStatement->expects($this->at(0))
66 | ->method('bindParam')
67 | ->with('key', 'test');
68 |
69 | $setStatement->expects($this->at(1))
70 | ->method('bindParam')
71 | ->with('value', 'value');
72 |
73 | $pdo = $this->mockPDO($this->prepareSQL(PDOStorageAdapter::STMT_SELECT), $getStatement);
74 |
75 | $pdo->expects($this->at(1))
76 | ->method('prepare')
77 | ->with($this->prepareSQL(PDOStorageAdapter::STMT_INSERT))
78 | ->willReturn($setStatement);
79 |
80 | $adapter = new PDOStorageAdapter($pdo, 'rollout_feature');
81 |
82 | $adapter->set('test', 'value');
83 | }
84 |
85 | public function testSetUpdate()
86 | {
87 | $getStatement = $this->mockPDOStatement();
88 | $getStatement->expects($this->once())
89 | ->method('fetch')
90 | ->willReturn(array('settings' => 'success'));
91 |
92 | $setStatement = $this->mockPDOStatement();
93 | $setStatement->expects($this->at(0))
94 | ->method('bindParam')
95 | ->with('key', 'test');
96 |
97 | $setStatement->expects($this->at(1))
98 | ->method('bindParam')
99 | ->with('value', 'value');
100 |
101 | $pdo = $this->mockPDO($this->prepareSQL(PDOStorageAdapter::STMT_SELECT), $getStatement);
102 |
103 | $this->mockPDOPrepare($pdo, 1, $this->prepareSQL(PDOStorageAdapter::STMT_UPDATE), $setStatement);
104 |
105 | $adapter = new PDOStorageAdapter($pdo, 'rollout_feature');
106 |
107 | $adapter->set('test', 'value');
108 | }
109 |
110 | private function prepareSQL($sql, $table = 'rollout_feature')
111 | {
112 | return str_replace(':table', $table, $sql);
113 | }
114 |
115 | /**
116 | * @return \PHPUnit_Framework_MockObject_MockObject
117 | */
118 | private function mockPDOStatement()
119 | {
120 | $statement = $this->getMockBuilder('\PDOStatement')
121 | ->getMock();
122 |
123 | return $statement;
124 | }
125 |
126 | /**
127 | * @param $query
128 | * @param $statement
129 | *
130 | * @return \PHPUnit_Framework_MockObject_MockObject
131 | */
132 | private function mockPDO($query, $statement)
133 | {
134 | $pdo = $this->getMockBuilder('\Opensoft\Tests\Storage\mockPDO')
135 | ->getMock();
136 |
137 | $this->mockPDOPrepare($pdo, 0, $query, $statement);
138 |
139 | return $pdo;
140 | }
141 |
142 | private function mockPDOPrepare(\PHPUnit_Framework_MockObject_MockObject $pdo, $at, $query, $statement)
143 | {
144 | $pdo->expects($this->at($at))
145 | ->method('prepare')
146 | ->with($query)
147 | ->willReturn($statement);
148 | }
149 | }
150 |
151 | class mockPDO extends \PDO
152 | {
153 | public function __construct()
154 | {
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/tests/Storage/RedisStorageAdapterTest.php:
--------------------------------------------------------------------------------
1 | redis = $this->getMockBuilder('\Opensoft\Tests\Storage\mockRedis')->getMock();
14 | }
15 |
16 | public function testGet()
17 | {
18 | $this->redis->expects($this->once())
19 | ->method('hget')
20 | ->with(RedisStorageAdapter::DEFAULT_GROUP, 'key')
21 | ->willReturn(json_encode('success'));
22 |
23 | $adapter = new RedisStorageAdapter($this->redis);
24 |
25 | $result = $adapter->get('key');
26 | $this->assertSame('success', $result);
27 | }
28 |
29 | public function testGetWithCustomGroup()
30 | {
31 | $this->redis->expects($this->once())
32 | ->method('hget')
33 | ->with('rollout_test', 'key')
34 | ->willReturn(json_encode('success'));
35 |
36 | $adapter = new RedisStorageAdapter($this->redis, 'rollout_test');
37 |
38 | $result = $adapter->get('key');
39 | $this->assertSame('success', $result);
40 | }
41 |
42 | public function testGetNotExistsFailure()
43 | {
44 | $this->redis->expects($this->once())
45 | ->method('hget')
46 | ->with(RedisStorageAdapter::DEFAULT_GROUP, 'key')
47 | ->willReturn('');
48 |
49 | $adapter = new RedisStorageAdapter($this->redis);
50 |
51 | $result = $adapter->get('key');
52 | $this->assertNull($result);
53 | }
54 |
55 | public function testGetJsonDecodeFailure()
56 | {
57 | $this->redis->expects($this->once())
58 | ->method('hget')
59 | ->with(RedisStorageAdapter::DEFAULT_GROUP, 'key')
60 | ->willReturn('not json');
61 |
62 | $adapter = new RedisStorageAdapter($this->redis);
63 |
64 | $result = $adapter->get('key');
65 | $this->assertNull($result);
66 | }
67 |
68 | public function testSet()
69 | {
70 | $this->redis->expects($this->once())
71 | ->method('hset')
72 | ->with(RedisStorageAdapter::DEFAULT_GROUP, 'key', json_encode('value'));
73 |
74 | $adapter = new RedisStorageAdapter($this->redis);
75 |
76 | $adapter->set('key', 'value');
77 | }
78 |
79 | public function testRemove()
80 | {
81 | $this->redis->expects($this->once())
82 | ->method('hdel')
83 | ->with(RedisStorageAdapter::DEFAULT_GROUP, 'key');
84 |
85 | $adapter = new RedisStorageAdapter($this->redis);
86 |
87 | $adapter->remove('key');
88 | }
89 |
90 | public function testSetWithCustomGroup()
91 | {
92 | $this->redis->expects($this->once())
93 | ->method('hset')
94 | ->with('rollout_test', 'key', json_encode('value'));
95 |
96 | $adapter = new RedisStorageAdapter($this->redis, 'rollout_test');
97 |
98 | $adapter->set('key', 'value');
99 | }
100 | }
101 |
102 | interface mockRedis
103 | {
104 | public function hget($key, $field);
105 | public function hset($key, $field, $value);
106 | public function hdel($key, $field);
107 | }
108 |
--------------------------------------------------------------------------------