|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Feature; |
|
4
|
|
|
|
|
5
|
|
|
use Feature\Contracts\User; |
|
6
|
|
|
use Feature\Contracts\World; |
|
7
|
|
|
|
|
8
|
|
|
/** |
|
9
|
|
|
* Class Config |
|
10
|
|
|
* @package Feature |
|
11
|
|
|
*/ |
|
12
|
|
|
class Config |
|
13
|
|
|
{ |
|
14
|
|
|
/* Keys used in a feature configuration. */ |
|
15
|
|
|
const DESCRIPTION = 'description'; |
|
16
|
|
|
const ENABLED = 'enabled'; |
|
17
|
|
|
const USERS = 'users'; |
|
18
|
|
|
const GROUPS = 'groups'; |
|
19
|
|
|
const ADMIN = 'admin'; |
|
20
|
|
|
const INTERNAL = 'internal'; |
|
21
|
|
|
const PUBLIC_URL_OVERRIDE = 'public_url_override'; |
|
22
|
|
|
const BUCKETING = 'bucketing'; |
|
23
|
|
|
|
|
24
|
|
|
/* Special values for enabled property. */ |
|
25
|
|
|
const ON = 'on'; /* Feature is fully enabled. */ |
|
26
|
|
|
const OFF = 'off'; /* Feature is fully disabled. */ |
|
27
|
|
|
|
|
28
|
|
|
/* Bucketing schemes. */ |
|
29
|
|
|
const UAID = 'uaid'; |
|
30
|
|
|
const USER = 'user'; |
|
31
|
|
|
const RANDOM = 'random'; |
|
32
|
|
|
|
|
33
|
|
|
/** |
|
34
|
|
|
* @var string |
|
35
|
|
|
*/ |
|
36
|
|
|
private $name; |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* @var array |
|
40
|
|
|
*/ |
|
41
|
|
|
private $cache; |
|
42
|
|
|
|
|
43
|
|
|
/** |
|
44
|
|
|
* @var World |
|
45
|
|
|
*/ |
|
46
|
|
|
private $world; |
|
47
|
|
|
|
|
48
|
|
|
private $description; |
|
49
|
|
|
private $enabled; |
|
50
|
|
|
private $users; |
|
51
|
|
|
private $groups; |
|
52
|
|
|
private $adminVariant; |
|
53
|
|
|
private $internalVariant; |
|
54
|
|
|
private $publicUrlOverride; |
|
55
|
|
|
private $bucketing; |
|
56
|
|
|
|
|
57
|
|
|
private $percentages; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* @param string $name |
|
61
|
|
|
* @param string|array $stanza |
|
62
|
|
|
* @param World $world |
|
63
|
|
|
*/ |
|
64
|
|
|
public function __construct($name, $stanza, World $world) |
|
65
|
|
|
{ |
|
66
|
|
|
$this->name = $name; |
|
67
|
|
|
$this->cache = array(); |
|
68
|
|
|
$this->world = $world; |
|
69
|
|
|
|
|
70
|
|
|
// Special case to save some memory--if the value is just a |
|
71
|
|
|
// string that is the same as setting enabled to that variant |
|
72
|
|
|
// (typically 'on' or 'off' but possibly another variant |
|
73
|
|
|
// name). This reduces the number of array objects we have to |
|
74
|
|
|
// create when reading the config file. |
|
75
|
|
|
if (is_null($stanza)) { |
|
76
|
|
|
$stanza = array(self::ENABLED => self::OFF); |
|
77
|
|
|
} elseif (is_string($stanza)) { |
|
78
|
|
|
$stanza = array(self::ENABLED => $stanza); |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
// Pull stuff from the config stanza. |
|
82
|
|
|
$this->description = $this->parseDescription($stanza); |
|
83
|
|
|
$this->enabled = $this->parseEnabled($stanza); |
|
84
|
|
|
$this->users = $this->parseUsersOrGroups($stanza, self::USERS); |
|
85
|
|
|
$this->groups = $this->parseUsersOrGroups($stanza, self::GROUPS); |
|
86
|
|
|
$this->adminVariant = $this->parseVariantName($stanza, self::ADMIN); |
|
87
|
|
|
$this->internalVariant = $this->parseVariantName($stanza, self::INTERNAL); |
|
88
|
|
|
$this->publicUrlOverride = $this->parsePublicURLOverride($stanza); |
|
89
|
|
|
$this->bucketing = $this->parseBucketBy($stanza); |
|
90
|
|
|
|
|
91
|
|
|
// Put the _enabled value into a more useful form for actually doing bucketing. |
|
92
|
|
|
$this->percentages = $this->computePercentages(); |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
//////////////////////////////////////////////////////////////////////// |
|
96
|
|
|
// Public API, though note that Feature.php is the only code that |
|
97
|
|
|
// should be using this class directly. |
|
98
|
|
|
|
|
99
|
|
|
/* |
|
100
|
|
|
* Is this feature enabled for the default id and the logged in |
|
101
|
|
|
* user, if any? |
|
102
|
|
|
*/ |
|
103
|
|
|
public function isEnabled() |
|
104
|
|
|
{ |
|
105
|
|
|
$bucketingId = $this->bucketingId(); |
|
106
|
|
|
$userId = $this->world->userId(); |
|
107
|
|
|
return $this->chooseVariant($bucketingId, $userId, false) !== self::OFF; |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
/* |
|
111
|
|
|
* What variant is enabled for the default id and the logged in |
|
112
|
|
|
* user, if any? |
|
113
|
|
|
*/ |
|
114
|
|
|
public function variant() |
|
115
|
|
|
{ |
|
116
|
|
|
$bucketingId = $this->bucketingId(); |
|
117
|
|
|
$userId = $this->world->userId(); |
|
118
|
|
|
return $this->chooseVariant($bucketingId, $userId, true); |
|
119
|
|
|
} |
|
120
|
|
|
|
|
121
|
|
|
/* |
|
122
|
|
|
* Is this feature enabled for the given user? |
|
123
|
|
|
*/ |
|
124
|
|
|
public function isEnabledFor(User $user) |
|
125
|
|
|
{ |
|
126
|
|
|
return $this->chooseVariant($user->getId(), $user->getId(), false) !== self::OFF; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
|
/* |
|
130
|
|
|
* Is this feature enabled, bucketing on the given bucketing |
|
131
|
|
|
* ID? (Other methods of enabling a feature and specifying a |
|
132
|
|
|
* variant such as users, groups, and query parameters, will still |
|
133
|
|
|
* work.) |
|
134
|
|
|
*/ |
|
135
|
|
|
public function isEnabledBucketingBy($bucketingId) |
|
136
|
|
|
{ |
|
137
|
|
|
$userId = $this->world->userId(); |
|
138
|
|
|
return $this->chooseVariant($bucketingId, $userId, false) !== self::OFF; |
|
139
|
|
|
} |
|
140
|
|
|
|
|
141
|
|
|
/* |
|
142
|
|
|
* What variant is enabled for the given user? |
|
143
|
|
|
*/ |
|
144
|
|
|
public function variantFor(User $user) |
|
145
|
|
|
{ |
|
146
|
|
|
return $this->chooseVariant($user->getId(), $user->getId(), true); |
|
147
|
|
|
} |
|
148
|
|
|
|
|
149
|
|
|
/* |
|
150
|
|
|
* What variant is enabled, bucketing on the given bucketing ID, |
|
151
|
|
|
* if any? |
|
152
|
|
|
*/ |
|
153
|
|
|
public function variantBucketingBy($bucketingId) |
|
154
|
|
|
{ |
|
155
|
|
|
$userId = $this->world->userId(); |
|
156
|
|
|
return $this->chooseVariant($bucketingId, $userId, true); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/* |
|
160
|
|
|
* Description of the feature. |
|
161
|
|
|
*/ |
|
162
|
|
|
public function description() |
|
163
|
|
|
{ |
|
164
|
|
|
return $this->description; |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
|
|
168
|
|
|
//////////////////////////////////////////////////////////////////////// |
|
169
|
|
|
// Internals |
|
170
|
|
|
|
|
171
|
|
|
/* |
|
172
|
|
|
* Get the name of the variant we should use. Returns OFF if the |
|
173
|
|
|
* feature is not enabled for $id. When $inVariantMethod is |
|
174
|
|
|
* true will also check the conditions that should hold for a |
|
175
|
|
|
* correct call to variant or variantFor: they should not be |
|
176
|
|
|
* called for features that are completely enabled (i.e. 'enabled' |
|
177
|
|
|
* => 'on') since all such variant-specific code should have been |
|
178
|
|
|
* cleaned up before changing the config and they should not be |
|
179
|
|
|
* called if the feature is, in fact, disabled for the given id |
|
180
|
|
|
* since those two methods should always be guarded by an |
|
181
|
|
|
* isEnabled/isEnabledFor call. |
|
182
|
|
|
* |
|
183
|
|
|
* @param $bucketingID the id used to assign a variant based on |
|
184
|
|
|
* the percentage of users that should see different variants. |
|
185
|
|
|
* |
|
186
|
|
|
* @param $userID the identity of the user to be used for the |
|
187
|
|
|
* special 'admin', 'users', and 'groups' access checks. |
|
188
|
|
|
* |
|
189
|
|
|
* @param $inVariantMethod were we called from variant or |
|
190
|
|
|
* variantFor, in which case we want to perform some certain |
|
191
|
|
|
* sanity checks to make sure the code is being used correctly. |
|
192
|
|
|
*/ |
|
193
|
|
|
private function chooseVariant($bucketingId, $userId, $inVariantMethod) |
|
194
|
|
|
{ |
|
195
|
|
|
if ($inVariantMethod && $this->enabled === self::ON) { |
|
196
|
|
|
$this->error("Variant check when fully enabled"); |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
if (is_string($this->enabled)) { |
|
200
|
|
|
// When enabled is on, off, or a variant name, that's the |
|
201
|
|
|
// end of the story. |
|
202
|
|
|
return $this->enabled; |
|
203
|
|
|
} else { |
|
204
|
|
|
if (is_null($bucketingId)) { |
|
205
|
|
|
throw new \InvalidArgumentException( |
|
206
|
|
|
"no bucketing ID supplied. if testing, configure feature " . |
|
207
|
|
|
"with enabled => 'on' or 'off', feature name = " . |
|
208
|
|
|
$this->name |
|
209
|
|
|
); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
$bucketingId = (string)$bucketingId; |
|
213
|
|
|
if (array_key_exists($bucketingId, $this->cache)) { |
|
214
|
|
|
// Note that this caching is not just an optimization: |
|
215
|
|
|
// it prevents us from double logging a single |
|
216
|
|
|
// feature--we only want to log each distinct checked |
|
217
|
|
|
// feature once. |
|
218
|
|
|
// |
|
219
|
|
|
// The caching also affects the semantics when we use |
|
220
|
|
|
// random bucketing (rather than hashing the id), i.e. |
|
221
|
|
|
// 'random' => 'true', by making the variant and |
|
222
|
|
|
// enabled status stable within a request. |
|
223
|
|
|
return $this->cache[$bucketingId]; |
|
224
|
|
|
} else { |
|
225
|
|
|
list($v, $selector) = |
|
226
|
|
|
$this->variantFromURL($userId) ?: |
|
227
|
|
|
$this->variantForUser($userId) ?: |
|
228
|
|
|
$this->variantForGroup($userId) ?: |
|
229
|
|
|
$this->variantForAdmin($userId) ?: |
|
230
|
|
|
$this->variantForInternal() ?: |
|
231
|
|
|
$this->variantByPercentage($bucketingId) ?: |
|
232
|
|
|
array(self::OFF, 'w'); |
|
233
|
|
|
|
|
234
|
|
|
if ($inVariantMethod && $v === self::OFF) { |
|
235
|
|
|
$this->error("Variant check outside enabled check"); |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
$this->world->log($this->name, $v, $selector); |
|
239
|
|
|
|
|
240
|
|
|
return $this->cache[$bucketingId] = $v; |
|
241
|
|
|
} |
|
242
|
|
|
} |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
/* |
|
246
|
|
|
* Return the globally accessible ID used by the one-arg isEnabled |
|
247
|
|
|
* and variant methods based on the feature's bucketing property. |
|
248
|
|
|
*/ |
|
249
|
|
|
private function bucketingId() |
|
250
|
|
|
{ |
|
251
|
|
|
switch ($this->bucketing) { |
|
252
|
|
|
case self::UAID: |
|
253
|
|
|
case self::RANDOM: |
|
254
|
|
|
// In the RANDOM case we still need a bucketing id to keep |
|
255
|
|
|
// the assignment stable within a request. |
|
256
|
|
|
// Note that when being run from outside of a web request (e.g. crons), |
|
257
|
|
|
// there is no UAID, so we default to a static string |
|
258
|
|
|
$uaid = $this->world->uaid(); |
|
259
|
|
|
return $uaid ? $uaid : "no uaid"; |
|
260
|
|
|
case self::USER: |
|
261
|
|
|
$userId = $this->world->userId(); |
|
262
|
|
|
// Not clear if this is right. There's an argument to be |
|
263
|
|
|
// made that if we're bucketing by userId and the user is |
|
264
|
|
|
// not logged in we should treat the feature as disabled. |
|
265
|
|
|
return !is_null($userId) ? $userId : $this->world->uaid(); |
|
266
|
|
|
default: |
|
267
|
|
|
throw new \InvalidArgumentException("Bad bucketing: $this->bucketing"); |
|
268
|
|
|
} |
|
269
|
|
|
} |
|
270
|
|
|
|
|
271
|
|
|
/* |
|
272
|
|
|
* For internal requests or if the feature has public_url_override |
|
273
|
|
|
* set to true, a specific variant can be specified in the |
|
274
|
|
|
* 'features' query parameter. In all other cases return false, |
|
275
|
|
|
* meaning nothing was specified. Note that foo:off will turn off |
|
276
|
|
|
* the 'foo' feature. |
|
277
|
|
|
*/ |
|
278
|
|
|
private function variantFromURL($userId) |
|
279
|
|
|
{ |
|
280
|
|
|
if ($this->publicUrlOverride or |
|
|
|
|
|
|
281
|
|
|
$this->world->isInternalRequest() or |
|
|
|
|
|
|
282
|
|
|
$this->world->isAdmin($userId) |
|
283
|
|
|
) { |
|
284
|
|
|
$urlFeatures = $this->world->urlFeatures(); |
|
285
|
|
|
if ($urlFeatures) { |
|
286
|
|
|
foreach (explode(',', $urlFeatures) as $f) { |
|
287
|
|
|
$parts = explode(':', $f); |
|
288
|
|
|
if ($parts[0] === $this->name) { |
|
289
|
|
|
return array(isset($parts[1]) ? $parts[1] : self::ON, 'o'); |
|
290
|
|
|
} |
|
291
|
|
|
} |
|
292
|
|
|
} |
|
293
|
|
|
} |
|
294
|
|
|
return false; |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
/* |
|
298
|
|
|
* Get the variant this user should see, if one was configured, |
|
299
|
|
|
* false otherwise. |
|
300
|
|
|
*/ |
|
301
|
|
|
private function variantForUser($userId) |
|
302
|
|
|
{ |
|
303
|
|
|
if ($this->users) { |
|
304
|
|
|
$name = $this->world->userName($userId); |
|
305
|
|
|
if ($name && array_key_exists($name, $this->users)) { |
|
306
|
|
|
return array($this->users[$name], 'u'); |
|
307
|
|
|
} |
|
308
|
|
|
} |
|
309
|
|
|
return false; |
|
310
|
|
|
} |
|
311
|
|
|
|
|
312
|
|
|
/* |
|
313
|
|
|
* Get the variant this user should see based on their group |
|
314
|
|
|
* memberships, if one was configured, false otherwise. N.B. If |
|
315
|
|
|
* the user is in multiple groups that are configured to see |
|
316
|
|
|
* different variants, they'll get the variant for one of their |
|
317
|
|
|
* groups but there's no saying which one. If this is a problem in |
|
318
|
|
|
* practice we could make the configuration more complex. Or you |
|
319
|
|
|
* can just provide a specific variant via the 'users' property. |
|
320
|
|
|
*/ |
|
321
|
|
|
private function variantForGroup($userId) |
|
322
|
|
|
{ |
|
323
|
|
|
if ($userId) { |
|
324
|
|
|
foreach ($this->groups as $groupId => $variant) { |
|
325
|
|
|
if ($this->world->inGroup($userId, $groupId)) { |
|
326
|
|
|
return array($variant, 'g'); |
|
327
|
|
|
} |
|
328
|
|
|
} |
|
329
|
|
|
} |
|
330
|
|
|
return false; |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
/* |
|
334
|
|
|
* What variant, if any, should we return if the current user is |
|
335
|
|
|
* an admin. |
|
336
|
|
|
*/ |
|
337
|
|
|
private function variantForAdmin($userId) |
|
338
|
|
|
{ |
|
339
|
|
|
if ($userId && $this->adminVariant) { |
|
340
|
|
|
if ($this->world->isAdmin($userId)) { |
|
341
|
|
|
return array($this->adminVariant, 'a'); |
|
342
|
|
|
} |
|
343
|
|
|
} |
|
344
|
|
|
return false; |
|
345
|
|
|
} |
|
346
|
|
|
|
|
347
|
|
|
/* |
|
348
|
|
|
* What variant, if any, should we return for internal requests. |
|
349
|
|
|
*/ |
|
350
|
|
|
private function variantForInternal() |
|
351
|
|
|
{ |
|
352
|
|
|
if ($this->internalVariant) { |
|
353
|
|
|
if ($this->world->isInternalRequest()) { |
|
354
|
|
|
return array($this->internalVariant, 'i'); |
|
355
|
|
|
} |
|
356
|
|
|
} |
|
357
|
|
|
return false; |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
/* |
|
361
|
|
|
* Finally, the normal case: use the percentage of users who |
|
362
|
|
|
* should see each variant to map a randomish number to a |
|
363
|
|
|
* particular variant. |
|
364
|
|
|
*/ |
|
365
|
|
|
private function variantByPercentage($id) |
|
366
|
|
|
{ |
|
367
|
|
|
$n = 100 * $this->randomish($id); |
|
368
|
|
|
foreach ($this->percentages as $v) { |
|
369
|
|
|
// === 100 check may not be necessary but I'm not good |
|
370
|
|
|
// enough numerical analyst to be sure. |
|
371
|
|
|
if ($n < $v[0] || $v[0] === 100) { |
|
372
|
|
|
return array($v[1], 'w'); |
|
373
|
|
|
} |
|
374
|
|
|
} |
|
375
|
|
|
return false; |
|
376
|
|
|
} |
|
377
|
|
|
|
|
378
|
|
|
/* |
|
379
|
|
|
* A randomish number in [0, 1) based on the feature name and $id |
|
380
|
|
|
* unless we are bucketing completely at random. |
|
381
|
|
|
*/ |
|
382
|
|
|
private function randomish($id) |
|
383
|
|
|
{ |
|
384
|
|
|
return $this->bucketing === self::RANDOM |
|
385
|
|
|
? Util::random() : Util::randomById($this->name . '-' . $id); |
|
386
|
|
|
} |
|
387
|
|
|
|
|
388
|
|
|
//////////////////////////////////////////////////////////////////////// |
|
389
|
|
|
// Configuration parsing |
|
390
|
|
|
|
|
391
|
|
|
private function parseDescription($stanza) |
|
392
|
|
|
{ |
|
393
|
|
|
return Util::arrayGet($stanza, self::DESCRIPTION, 'No description.'); |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
|
|
/* |
|
397
|
|
|
* Parse the 'enabled' property of the feature's config stanza. |
|
398
|
|
|
*/ |
|
399
|
|
|
private function parseEnabled($stanza) |
|
400
|
|
|
{ |
|
401
|
|
|
$enabled = Util::arrayGet($stanza, self::ENABLED, 0); |
|
402
|
|
|
|
|
403
|
|
|
if (is_numeric($enabled)) { |
|
404
|
|
|
if ($enabled < 0) { |
|
405
|
|
|
$this->error("enabled ($enabled) < 0"); |
|
406
|
|
|
$enabled = 0; |
|
407
|
|
|
} elseif ($enabled > 100) { |
|
408
|
|
|
$this->error("enabled ($enabled) > 100"); |
|
409
|
|
|
$enabled = 100; |
|
410
|
|
|
} |
|
411
|
|
|
return array('on' => $enabled); |
|
412
|
|
|
|
|
413
|
|
|
} elseif (is_string($enabled) or is_array($enabled)) { |
|
|
|
|
|
|
414
|
|
|
return $enabled; |
|
415
|
|
|
} |
|
416
|
|
|
$this->error("Malformed enabled property"); |
|
417
|
|
|
} |
|
418
|
|
|
|
|
419
|
|
|
/* |
|
420
|
|
|
* Returns an array of pairs with the first element of the pair |
|
421
|
|
|
* being the upper-boundary of the variants percentage and the |
|
422
|
|
|
* second element being the name of the variant. |
|
423
|
|
|
*/ |
|
424
|
|
|
private function computePercentages() |
|
425
|
|
|
{ |
|
426
|
|
|
$total = 0; |
|
427
|
|
|
$percentages = array(); |
|
428
|
|
|
if (is_array($this->enabled)) { |
|
429
|
|
|
foreach ($this->enabled as $variant => $percentage) { |
|
430
|
|
|
if (!is_numeric($percentage) || $percentage < 0 || $percentage > 100) { |
|
431
|
|
|
$this->error("Bad percentage $percentage"); |
|
432
|
|
|
} |
|
433
|
|
|
if ($percentage > 0) { |
|
434
|
|
|
$total += $percentage; |
|
435
|
|
|
$percentages[] = array($total, $variant); |
|
436
|
|
|
} |
|
437
|
|
|
if ($total > 100) { |
|
438
|
|
|
$this->error("Total of percentages > 100: $total"); |
|
439
|
|
|
} |
|
440
|
|
|
} |
|
441
|
|
|
} |
|
442
|
|
|
return $percentages; |
|
443
|
|
|
} |
|
444
|
|
|
|
|
445
|
|
|
/* |
|
446
|
|
|
* Parse the value of the 'users' and 'groups' properties of the |
|
447
|
|
|
* feature's config stanza, returning an array mappinng the user |
|
448
|
|
|
* or group names to they variant they should see. |
|
449
|
|
|
*/ |
|
450
|
|
|
private function parseUsersOrGroups($stanza, $what) |
|
451
|
|
|
{ |
|
452
|
|
|
$value = Util::arrayGet($stanza, $what); |
|
453
|
|
|
if (is_string($value) || is_numeric($value)) { |
|
454
|
|
|
// Users are configrued with their user names. Groups as |
|
455
|
|
|
// numeric ids. (Not sure if that's a great idea.) |
|
456
|
|
|
return array($value => self::ON); |
|
457
|
|
|
|
|
458
|
|
|
} elseif (self::isList($value)) { |
|
459
|
|
|
$result = array(); |
|
460
|
|
|
foreach ($value as $who) { |
|
|
|
|
|
|
461
|
|
|
$result[strtolower($who)] = self::ON; |
|
462
|
|
|
} |
|
463
|
|
|
return $result; |
|
464
|
|
|
|
|
465
|
|
|
} elseif (is_array($value)) { |
|
466
|
|
|
$result = array(); |
|
467
|
|
|
$bad_keys = is_array($this->enabled) ? |
|
468
|
|
|
array_keys(array_diff_key($value, $this->enabled)) : |
|
469
|
|
|
array(); |
|
470
|
|
|
if (!$bad_keys) { |
|
|
|
|
|
|
471
|
|
|
foreach ($value as $variant => $whos) { |
|
472
|
|
|
foreach (self::asArray($whos) as $who) { |
|
473
|
|
|
$result[strtolower($who)] = $variant; |
|
474
|
|
|
} |
|
475
|
|
|
} |
|
476
|
|
|
return $result; |
|
477
|
|
|
|
|
478
|
|
|
} else { |
|
479
|
|
|
$this->error("Unknown variants " . implode(', ', $bad_keys)); |
|
480
|
|
|
} |
|
481
|
|
|
} |
|
482
|
|
|
return array(); |
|
483
|
|
|
} |
|
484
|
|
|
|
|
485
|
|
|
/* |
|
486
|
|
|
* Parse the variant name value for the 'admin' and 'internal' |
|
487
|
|
|
* properties. If non-falsy, must be one of the keys in the |
|
488
|
|
|
* enabled map unless enabled is 'on' or 'off'. |
|
489
|
|
|
*/ |
|
490
|
|
|
private function parseVariantName($stanza, $what) |
|
491
|
|
|
{ |
|
492
|
|
|
$value = Util::arrayGet($stanza, $what); |
|
493
|
|
|
if ($value) { |
|
494
|
|
|
if (is_array($this->enabled)) { |
|
495
|
|
|
if (array_key_exists($value, $this->enabled)) { |
|
496
|
|
|
return $value; |
|
497
|
|
|
} else { |
|
498
|
|
|
$this->error("Unknown variant $value"); |
|
499
|
|
|
} |
|
500
|
|
|
} else { |
|
501
|
|
|
return $value; |
|
502
|
|
|
} |
|
503
|
|
|
} |
|
504
|
|
|
return false; |
|
505
|
|
|
} |
|
506
|
|
|
|
|
507
|
|
|
/** |
|
508
|
|
|
* @param array $stanza |
|
509
|
|
|
* @return mixed|null |
|
510
|
|
|
*/ |
|
511
|
|
|
private function parsePublicURLOverride($stanza) |
|
512
|
|
|
{ |
|
513
|
|
|
return Util::arrayGet($stanza, self::PUBLIC_URL_OVERRIDE, false); |
|
514
|
|
|
} |
|
515
|
|
|
|
|
516
|
|
|
/** |
|
517
|
|
|
* @param array $stanza |
|
518
|
|
|
* @return mixed|null |
|
519
|
|
|
*/ |
|
520
|
|
|
private function parseBucketBy($stanza) |
|
521
|
|
|
{ |
|
522
|
|
|
return Util::arrayGet($stanza, self::BUCKETING, self::UAID); |
|
523
|
|
|
} |
|
524
|
|
|
|
|
525
|
|
|
//////////////////////////////////////////////////////////////////////// |
|
526
|
|
|
// Genericish utilities |
|
527
|
|
|
|
|
528
|
|
|
/* |
|
529
|
|
|
* Is the given object an array value that could have been created |
|
530
|
|
|
* with array(...) with no =>'s in the ...? |
|
531
|
|
|
*/ |
|
532
|
|
|
private static function isList($a) |
|
533
|
|
|
{ |
|
534
|
|
|
return is_array($a) and array_keys($a) === range(0, count($a) - 1); |
|
|
|
|
|
|
535
|
|
|
} |
|
536
|
|
|
|
|
537
|
|
|
/** |
|
538
|
|
|
* @param mixed $x |
|
539
|
|
|
* @return array |
|
540
|
|
|
*/ |
|
541
|
|
|
private static function asArray($x) |
|
542
|
|
|
{ |
|
543
|
|
|
return is_array($x) ? $x : array($x); |
|
544
|
|
|
} |
|
545
|
|
|
|
|
546
|
|
|
/** |
|
547
|
|
|
* @param $message |
|
548
|
|
|
*/ |
|
549
|
|
|
private function error($message) |
|
550
|
|
|
{ |
|
551
|
|
|
throw new \InvalidArgumentException($message); |
|
552
|
|
|
} |
|
553
|
|
|
} |
|
554
|
|
|
|
PHP has two types of connecting operators (logical operators, and boolean operators):
and&&or||The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like
&&, or||.Let’s take a look at a few examples:
Logical Operators are used for Control-Flow
One case where you explicitly want to use logical operators is for control-flow such as this:
Since
dieintroduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined withthrowat this point:These limitations lead to logical operators rarely being of use in current PHP code.