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
die
introduces 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 withthrow
at this point:These limitations lead to logical operators rarely being of use in current PHP code.