Skip to content

Commit 3fbbc79

Browse files
committed
SmartNull: compare method names case-insensitively in __call
PHP ignores case on method names, but the map/apply and getIterator checks used ===, so ->Map() delegated to SmartString and ran the callback on null (fatal with a typed callback) where ->map() correctly propagates. All name checks now use a strtolower copy, matching the case-insensitive delegation. Also fixes security scan F6 (same finding). Mixed-case test added.
1 parent 89b286c commit 3fbbc79

2 files changed

Lines changed: 20 additions & 3 deletions

File tree

src/SmartNull.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,15 +309,16 @@ public function __call($name, array $arguments): mixed
309309
// The in_array list mirrors the deprecated shims in SmartString::__call, which
310310
// method_exists() can't see. Keep both sites in sync: when SmartString drops a
311311
// shim, drop it here too.
312+
$nameLower = strtolower($name); // PHP method dispatch ignores case, so every name check here must too
312313
$isSmartStringMethod = $this->useSmartStrings
313-
&& $name !== 'getIterator'
314+
&& $nameLower !== 'getiterator'
314315
&& (
315316
(method_exists(SmartString::class, $name) && (new ReflectionMethod(SmartString::class, $name))->isPublic())
316-
|| in_array(strtolower($name), ['noencode', 'tostring', 'jsencode', 'striptags'], true)
317+
|| in_array($nameLower, ['noencode', 'tostring', 'jsencode', 'striptags'], true)
317318
);
318319

319320
if ($isSmartStringMethod) {
320-
if ($name === 'map' || $name === 'apply') {
321+
if ($nameLower === 'map' || $nameLower === 'apply') {
321322
return $this;
322323
}
323324
$result = SmartString::new(null)->$name(...$arguments);

tests/Unit/SmartNullTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,22 @@ public function testApplyAliasPropagatesLikeMapInHtmlMode(): void
307307
$this->assertSame('n/a', $smartNull->apply('strtoupper')->or('n/a')->value(), 'chain stays open after apply');
308308
}
309309

310+
public function testOddCasedMapAndApplyPropagateLikeLowercaseInHtmlMode(): void
311+
{
312+
// PHP method dispatch ignores case (->dateformat() works), so ->Map() must
313+
// propagate exactly like ->map() - not delegate and run the callback on null
314+
$smartNull = $this->smartNullFrom(SmartArrayHtml::class);
315+
$calls = 0;
316+
$callback = function ($value) use (&$calls) {
317+
$calls++;
318+
return 'computed';
319+
};
320+
321+
$this->assertSame($smartNull, $smartNull->Map($callback));
322+
$this->assertSame($smartNull, $smartNull->APPLY($callback));
323+
$this->assertSame(0, $calls, 'the callback never runs on a missing key, whatever the casing');
324+
}
325+
310326
public function testDeprecatedSmartStringShimsWorkOnMissingKeysInHtmlMode(): void
311327
{
312328
// noEncode/toString/jsEncode/stripTags only exist inside SmartString's

0 commit comments

Comments
 (0)