Skip to content

Commit 88a52c1

Browse files
committed
SmartNull: bracket access signals through $onOffsetAccess like a real row
SmartNull's ArrayAccess methods skipped the deprecation dispatcher, so bracket calls on missing-data paths (empty result, missing key) were silent in every mode - a migration sweep with 'throw' missed exactly the call sites that only run when data is absent. triggerArrayAccessDeprecation() is now a public static on the Deprecations trait and SmartNull's get/set/unset call it. offsetExists stays silent, matching the base class, so isset() and ?? don't double-notify. Tests added; SmartNull chaining tests updated to expect the notices.
1 parent b0faab1 commit 88a52c1

5 files changed

Lines changed: 74 additions & 9 deletions

File tree

src/Deprecations.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ public function chunk(int $size): static
417417
*/
418418
public function offsetSet(mixed $offset, mixed $value): void
419419
{
420-
$this->triggerArrayAccessDeprecation($offset, 'set');
420+
self::triggerArrayAccessDeprecation($offset, 'set');
421421
$this->setElement($offset, $value);
422422
}
423423

@@ -434,7 +434,7 @@ public function offsetGet(mixed $offset): static|SmartNull|SmartString|string|in
434434
is_float($offset), is_bool($offset) => (int) $offset,
435435
default => $offset,
436436
};
437-
$this->triggerArrayAccessDeprecation($offset, 'get');
437+
self::triggerArrayAccessDeprecation($offset, 'get');
438438
return $this->getElement($offset);
439439
}
440440

@@ -459,7 +459,7 @@ public function offsetExists(mixed $offset): bool
459459
*/
460460
public function offsetUnset(mixed $offset): void
461461
{
462-
$this->triggerArrayAccessDeprecation($offset, 'unset');
462+
self::triggerArrayAccessDeprecation($offset, 'unset');
463463
$this->sourceRows = null; // same staleness rule as setElement()
464464
$this->root->sourceRows = null;
465465
unset($this->data[$offset]);
@@ -468,9 +468,13 @@ public function offsetUnset(mixed $offset): void
468468
/**
469469
* Surface a deprecation notice for array access syntax, dispatched per $onOffsetAccess mode.
470470
*
471+
* Public static so SmartNull, which defines its own ArrayAccess methods, dispatches
472+
* through the same rules and modes.
473+
*
474+
* @internal not part of the supported API
471475
* @see SmartArrayBase::$onOffsetAccess
472476
*/
473-
private function triggerArrayAccessDeprecation(mixed $key, string $operation = 'get'): void
477+
public static function triggerArrayAccessDeprecation(mixed $key, string $operation = 'get'): void
474478
{
475479
// SECURITY: the key can be user input (e.g. $arr[$_GET['sort']]) and 'notify' mode echoes
476480
// the message into the page, so encode it. $key is display-only from here on; the actual

src/SmartNull.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,11 @@ public function count(): int
181181

182182
/**
183183
* Array reads on a missing value stay missing: returns $this so chains keep working.
184+
* Bracket syntax is deprecated everywhere, so it dispatches per $onOffsetAccess first.
184185
*/
185186
public function offsetGet(mixed $offset): SmartNull
186187
{
188+
SmartArrayBase::triggerArrayAccessDeprecation($offset, 'get');
187189
return $this;
188190
}
189191

@@ -192,11 +194,13 @@ public function offsetGet(mixed $offset): SmartNull
192194
*/
193195
public function offsetSet(mixed $offset, mixed $value): void
194196
{
197+
SmartArrayBase::triggerArrayAccessDeprecation($offset, 'set');
195198
$this->throwCannotSet();
196199
}
197200

198201
/**
199-
* No keys exist on a missing value.
202+
* No keys exist on a missing value. Silent like SmartArrayBase::offsetExists,
203+
* so isset() and ?? don't signal - the read carries the notice.
200204
*/
201205
public function offsetExists(mixed $offset): bool
202206
{
@@ -205,7 +209,8 @@ public function offsetExists(mixed $offset): bool
205209

206210
public function offsetUnset(mixed $offset): void
207211
{
208-
// ArrayAccess requires this; a SmartNull has nothing to unset
212+
// Nothing to unset, but the deprecated bracket syntax still signals
213+
SmartArrayBase::triggerArrayAccessDeprecation($offset, 'unset');
209214
}
210215

211216
//endregion

tests/Unit/DeprecationsTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public function testTraitPublicMethodInventoryIsPinned(): void
7575
'sprintf',
7676
'toHtml',
7777
'toRaw',
78+
'triggerArrayAccessDeprecation', // not an alias: the offset-syntax dispatcher, public so SmartNull can call it
7879
'withSmartStrings',
7980
], $methods);
8081
}

tests/Unit/GlobalSettingsTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Itools\SmartArray\SmartArray;
99
use Itools\SmartArray\SmartArrayBase;
1010
use Itools\SmartArray\SmartArrayHtml;
11+
use Itools\SmartArray\SmartNull;
1112
use Itools\SmartArray\Tests\Support\SmartArrayTestCase;
1213
use PHPUnit\Framework\Attributes\DataProvider;
1314
use ReflectionClass;
@@ -497,6 +498,60 @@ public function testNestedReadChainSignalsOncePerLevel(): void
497498
], $this->normalizeCaller($deprecations));
498499
}
499500

501+
//endregion
502+
//region SmartNull (missing keys and empty results)
503+
504+
#[DataProvider('modeProvider')]
505+
public function testSmartNullBracketReadDispatchesLikeARealRow(string $class): void
506+
{
507+
// Missing-data paths signal too, so a migration sweep also finds the
508+
// bracket call sites that only run when a result is empty
509+
$smartNull = $class::new([])->first();
510+
511+
[[$result, $output], $deprecations] = $this->withOffsetAccess('notify', fn() => $this->captureDeprecations(
512+
fn() => $this->captureOutput(fn() => $smartNull['name'])
513+
));
514+
515+
$this->assertInstanceOf(SmartNull::class, $result, 'the read still chains');
516+
$this->assertSame($this->expectedEcho(["Replace ['name'] with ->name"]), $this->normalizeCaller($output));
517+
$this->assertSame($this->expectedMessages(["Replace ['name'] with ->name"]), $this->normalizeCaller($deprecations));
518+
}
519+
520+
#[DataProvider('modeProvider')]
521+
public function testSmartNullBracketAccessThrowsInThrowMode(string $class): void
522+
{
523+
$smartNull = $class::new([])->first();
524+
525+
$read = $this->withOffsetAccess('throw', fn() => $this->catchThrowable(fn() => $smartNull['name']));
526+
$this->assertInstanceOf(RuntimeException::class, $read);
527+
$this->assertSame("Replace ['name'] with ->name in FILE:LINE.", $this->normalizeCaller($read->getMessage()));
528+
529+
$write = $this->withOffsetAccess('throw', fn() => $this->catchThrowable(fn() => $smartNull['name'] = 'x'));
530+
$this->assertInstanceOf(RuntimeException::class, $write);
531+
$this->assertSame("Replace ['name'] with ->name = \$value in FILE:LINE.", $this->normalizeCaller($write->getMessage()));
532+
533+
$unset = $this->withOffsetAccess('throw', fn() => $this->catchThrowable(function () use ($smartNull) {
534+
unset($smartNull['name']);
535+
}));
536+
$this->assertInstanceOf(RuntimeException::class, $unset);
537+
}
538+
539+
#[DataProvider('modeProvider')]
540+
public function testSmartNullExistenceChecksStaySilent(string $class): void
541+
{
542+
// offsetExists stays silent like SmartArrayBase's, so isset() and ??
543+
// don't signal on missing-data paths either - even in throw mode
544+
$smartNull = $class::new([])->first();
545+
546+
[[$results, $output], $deprecations] = $this->withOffsetAccess('throw', fn() => $this->captureDeprecations(
547+
fn() => $this->captureOutput(fn() => [isset($smartNull['name']), $smartNull['name'] ?? 'default'])
548+
));
549+
550+
$this->assertSame([false, 'default'], $results);
551+
$this->assertSame('', $output);
552+
$this->assertSame([], $deprecations);
553+
}
554+
500555
//endregion
501556
//region Helpers
502557

tests/Unit/SmartNullTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ public function testArrayAccessChainingReturnsTheSameSmartNull(string $class): v
146146

147147
[$result, $output] = $this->captureOutput(fn() => $smartNull['a']['b'][0]);
148148

149-
$this->assertSame($smartNull, $result, 'array reads on SmartNull are exempt from the offset-access deprecation');
150-
$this->assertSame('', $output);
149+
$this->assertSame($smartNull, $result, 'array reads return $this, so chains keep working');
150+
$this->assertSame(3, substr_count($output, "\nDeprecated: "), 'bracket syntax notifies on missing-data paths like anywhere else');
151151
}
152152

153153
#[DataProvider('modeProvider')]
@@ -158,7 +158,7 @@ public function testMixedPropertyArrayAndMethodChainResolvesToNull(string $class
158158
[$result, $output] = $this->captureOutput(fn() => $smartNull['rows']->first()->name->value());
159159

160160
$this->assertNull($result, 'the whole chain resolves to null with no fatal');
161-
$this->assertSame('', $output);
161+
$this->assertSame(1, substr_count($output, "\nDeprecated: "), 'the one bracket read notifies');
162162
}
163163

164164
#[DataProvider('modeProvider')]

0 commit comments

Comments
 (0)