|
20 | 20 | * Covers the full matrix: modes (notify/log/throw) x operations (offsetGet, |
21 | 21 | * offsetSet, append, offsetUnset, offsetExists via isset() and empty()), the |
22 | 22 | * exact suggestion text for each key shape, and the invalid-mode error. Also |
23 | | - * pins what must stay signal-free: property syntax and the library's own |
24 | | - * internal element access. |
| 23 | + * pins what must stay signal-free: property syntax, existence checks |
| 24 | + * (offsetExists - the read carries the notice for `??` and empty()), and the |
| 25 | + * library's own internal element access. |
25 | 26 | * |
26 | 27 | * Every test swaps the static through withOffsetAccess(), which restores it in a |
27 | 28 | * finally block so a failure here cannot poison other test files. |
@@ -68,18 +69,19 @@ private static function offsetOperations(): array |
68 | 69 | 'unset int key' => [function (SmartArrayBase $sa) { unset($sa[0]); }, ["Replace [0] with ->{0}"]], |
69 | 70 | 'unset invalid prop name' => [function (SmartArrayBase $sa) { unset($sa['users.id']); }, ["Replace ['users.id'] with ->{'users.id'}"]], |
70 | 71 |
|
71 | | - // offsetExists via isset(): one call, whether or not the key is there |
72 | | - 'isset string key' => [fn(SmartArrayBase $sa) => isset($sa['name']), ["Replace ['name'] with ->name"]], |
73 | | - 'isset int key' => [fn(SmartArrayBase $sa) => isset($sa[0]), ["Replace [0] with ->{0}"]], |
74 | | - 'isset invalid prop name' => [fn(SmartArrayBase $sa) => isset($sa['users.id']), ["Replace ['users.id'] with ->{'users.id'}"]], |
75 | | - 'isset missing key' => [fn(SmartArrayBase $sa) => isset($sa['zzz']), ["Replace ['zzz'] with ->zzz"]], |
76 | | - |
77 | | - // offsetExists via empty(): PHP calls offsetExists, then offsetGet when the key exists, |
78 | | - // so an existing key signals twice for one empty() check - both notices give the |
79 | | - // same suggestion (reads and existence checks share one suggestion style) |
80 | | - 'empty existing key' => [fn(SmartArrayBase $sa) => empty($sa['name']), ["Replace ['name'] with ->name", "Replace ['name'] with ->name"]], |
81 | | - 'empty int key' => [fn(SmartArrayBase $sa) => empty($sa[0]), ["Replace [0] with ->{0}", "Replace [0] with ->{0}"]], |
82 | | - 'empty missing key' => [fn(SmartArrayBase $sa) => empty($sa['zzz']), ["Replace ['zzz'] with ->zzz"]], |
| 72 | + // offsetExists via isset(): silent - PHP also calls offsetGet() for `??` and |
| 73 | + // empty(), which carries the one notice, so a notice here would double every |
| 74 | + // message. A bare isset() gives no signal but still returns the right answer. |
| 75 | + 'isset string key' => [fn(SmartArrayBase $sa) => isset($sa['name']), []], |
| 76 | + 'isset int key' => [fn(SmartArrayBase $sa) => isset($sa[0]), []], |
| 77 | + 'isset invalid prop name' => [fn(SmartArrayBase $sa) => isset($sa['users.id']), []], |
| 78 | + 'isset missing key' => [fn(SmartArrayBase $sa) => isset($sa['zzz']), []], |
| 79 | + |
| 80 | + // offsetExists via empty(): offsetExists is silent, then PHP calls offsetGet |
| 81 | + // when the key exists - one notice for existing keys, none for missing ones |
| 82 | + 'empty existing key' => [fn(SmartArrayBase $sa) => empty($sa['name']), ["Replace ['name'] with ->name"]], |
| 83 | + 'empty int key' => [fn(SmartArrayBase $sa) => empty($sa[0]), ["Replace [0] with ->{0}"]], |
| 84 | + 'empty missing key' => [fn(SmartArrayBase $sa) => empty($sa['zzz']), []], |
83 | 85 | ]; |
84 | 86 | } |
85 | 87 |
|
@@ -140,7 +142,7 @@ public function testNotifyPerformsTheOperation(string $class): void |
140 | 142 | $this->assertTrue($issetName); |
141 | 143 | $this->assertFalse($issetMissing); |
142 | 144 | $this->assertSame(['name' => 'Bob', '' => 'blank', 0 => 'zero', 'city' => 'Vancouver', 1 => 'appended'], $sa->toArray()); |
143 | | - $this->assertSame(6, substr_count($output, "\nDeprecated: "), 'one notice per offset operation'); |
| 145 | + $this->assertSame(4, substr_count($output, "\nDeprecated: "), 'one notice per read/write; isset checks are silent'); |
144 | 146 | } |
145 | 147 |
|
146 | 148 | //endregion |
@@ -190,8 +192,12 @@ public function testThrowRaisesRuntimeExceptionWithTheSuggestion(string $class, |
190 | 192 | fn() => $this->captureOutput(fn() => $this->catchThrowable(fn() => $operation($sa))) |
191 | 193 | )); |
192 | 194 |
|
193 | | - $this->assertInstanceOf(RuntimeException::class, $thrown); |
194 | | - $this->assertSame($expectedMessages[0] . ' in FILE:LINE.', $this->normalizeCaller($thrown->getMessage())); |
| 195 | + if ($expectedMessages === []) { |
| 196 | + $this->assertNull($thrown, 'signal-free operations run normally in throw mode'); |
| 197 | + } else { |
| 198 | + $this->assertInstanceOf(RuntimeException::class, $thrown); |
| 199 | + $this->assertSame($expectedMessages[0] . ' in FILE:LINE.', $this->normalizeCaller($thrown->getMessage())); |
| 200 | + } |
195 | 201 | $this->assertSame('', $output, 'throw mode replaces the notice, it does not add to it'); |
196 | 202 | $this->assertSame([], $deprecations, 'throw mode exits before trigger_error()'); |
197 | 203 | } |
@@ -287,7 +293,7 @@ public function testInvalidModeThrowsForEveryOffsetOperation(): void |
287 | 293 | 'set' => function () use ($sa) { $sa['city'] = 'Vancouver'; }, |
288 | 294 | 'append' => function () use ($sa) { $sa[] = 'appended'; }, |
289 | 295 | 'unset' => function () use ($sa) { unset($sa['name']); }, |
290 | | - 'isset' => fn() => isset($sa['name']), |
| 296 | + // no 'isset' row: offsetExists is signal-free and never consults the setting |
291 | 297 | ]; |
292 | 298 |
|
293 | 299 | foreach ($operations as $label => $operation) { |
@@ -431,36 +437,34 @@ public function testNullOffsetExistsAndUnsetUseTheEmptyStringKey(): void |
431 | 437 |
|
432 | 438 | $this->assertTrue($exists); |
433 | 439 | $this->assertSame([], $sa->toArray()); |
| 440 | + |
| 441 | + // One notice, from the unset - the isset is signal-free |
434 | 442 | $this->assertSame( |
435 | | - "\nDeprecated: Replace [] with ->get('') in FILE:LINE.\n\nDeprecated: Replace [] with ->get('') in FILE:LINE.\n", |
| 443 | + "\nDeprecated: Replace [] with ->get('') in FILE:LINE.\n", |
436 | 444 | $this->normalizeCaller($output), |
437 | 445 | ); |
438 | 446 | $this->assertSame( |
439 | | - ["Replace [] with ->get('') in FILE:LINE.", "Replace [] with ->get('') in FILE:LINE."], |
| 447 | + ["Replace [] with ->get('') in FILE:LINE."], |
440 | 448 | $this->normalizeCaller($deprecations), |
441 | 449 | ); |
442 | 450 | } |
443 | 451 |
|
444 | 452 | //endregion |
445 | 453 | //region Nested chains |
446 | 454 |
|
447 | | - public function testNestedIssetChainSignalsThreeTimes(): void |
| 455 | + public function testNestedIssetChainSignalsOnce(): void |
448 | 456 | { |
449 | | - // isset($sa['user']['name']) reports ['user'] twice - PHP checks the outer key |
450 | | - // with offsetExists, then reads it with offsetGet to reach the inner one. That |
451 | | - // call sequence is PHP's, not ours; all notices agree on the suggestion. |
| 457 | + // isset($sa['user']['name']): PHP checks the outer key with offsetExists |
| 458 | + // (silent), reads it with offsetGet (the one notice) to reach the inner |
| 459 | + // offsetExists (silent). That call sequence is PHP's, not ours. |
452 | 460 | $sa = SmartArray::new(['user' => ['name' => 'Bob']]); |
453 | 461 |
|
454 | 462 | [$exists, $deprecations] = $this->withOffsetAccess('log', fn() => $this->captureDeprecations( |
455 | 463 | fn() => isset($sa['user']['name']) |
456 | 464 | )); |
457 | 465 |
|
458 | 466 | $this->assertTrue($exists); |
459 | | - $this->assertSame([ |
460 | | - "Replace ['user'] with ->user in FILE:LINE.", |
461 | | - "Replace ['user'] with ->user in FILE:LINE.", |
462 | | - "Replace ['name'] with ->name in FILE:LINE.", |
463 | | - ], $this->normalizeCaller($deprecations)); |
| 467 | + $this->assertSame(["Replace ['user'] with ->user in FILE:LINE."], $this->normalizeCaller($deprecations)); |
464 | 468 | } |
465 | 469 |
|
466 | 470 | public function testNestedReadChainSignalsOncePerLevel(): void |
|
0 commit comments