Skip to content

Commit f1f5cc3

Browse files
committed
SmartNull: don't answer SmartString methods when SmartStrings are off
- Before: ->or(), ->trim(), etc. on a missing key in a raw array quietly worked and returned an HTML-encoding SmartString - the one place a raw array handed back encoded output - Now: throws the same "Call to undefined method" error as any unknown method, matching stored values (plain strings, so those calls always failed there) - HTML mode unchanged - misses still chain - Fallbacks on raw arrays use ?? instead
1 parent 2a50587 commit f1f5cc3

3 files changed

Lines changed: 25 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- `orDie()` and `or404()` now exit with status 1 instead of 0, so shell scripts and cron jobs see the failure. Output is unchanged. Matches SmartString.
3737
- Developer-mistake exceptions (bad types, wrong context, misuse) now throw `CallerException`, which reports your file and line instead of the library's internals - the same class SmartString uses. It extends `InvalidArgumentException`, so existing catch blocks keep working, except six throws that previously used `RuntimeException`: `load()` misuse (no handler, non-callable handler, bad or empty field name, called on a record set), `orRedirect()` after headers sent, and writing to a `SmartNull`. See UPGRADING.md. `orThrow()` still throws `RuntimeException` by contract.
3838
- Clearer messages for two of those throws: `load()` with no handler now explains handlers come from the database layer (was "No loadHandler property is defined"), and writing to a `SmartNull` now says the value came from a missing key or empty result (was "Cannot set values on SmartNull"). The unsupported-type message from `set()` no longer prefixes the library's internal method name.
39+
- Raw-mode arrays no longer answer SmartString methods on missing keys: `$row->missing->or('n/a')` on a raw array throws the standard undefined-method Error instead of returning an HTML-encoding SmartString. Raw stored values never had these methods (chaining `->or()` on a stored string was already a fatal), so a miss was the one path that silently produced encoded output in a raw array. HTML mode is unchanged - SmartNull still delegates SmartString methods so chains through a missing key keep working. Raw fallbacks use `??`.
3940
- Unknown methods on `SmartNull` now throw the same `Error` as the rest of the library - method name, "did you mean" hint, caller's file and line - instead of `InvalidArgumentException("Method 'x' not found")`. Chains from a missing key now fail with the same message quality as everything else.
4041

4142
### Deprecated

src/SmartNull.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,10 @@ private function throwCannotSet(): never
252252
*/
253253
public function __call($name, array $arguments): mixed
254254
{
255-
if (!method_exists(SmartArrayBase::class, $name) && method_exists(SmartString::class, $name)) {
255+
// SmartString methods only delegate in HTML mode: raw values are plain scalars
256+
// with no methods, so a miss answers SmartString calls the same way - with the
257+
// standard undefined-method Error
258+
if ($this->useSmartStrings && !method_exists(SmartArrayBase::class, $name) && method_exists(SmartString::class, $name)) {
256259
return SmartString::new(null)->$name(...$arguments);
257260
}
258261
return $this->useSmartStrings

tests/Unit/SmartNullTest.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,9 @@ public function testDelegatedArraysCarryTheSourceMetadata(string $class): void
224224
//endregion
225225
//region Method delegation: SmartString methods
226226

227-
#[DataProvider('modeProvider')]
228-
public function testSmartStringOnlyMethodsDelegateToANullSmartString(string $class): void
227+
public function testSmartStringOnlyMethodsDelegateToANullSmartStringInHtmlMode(): void
229228
{
230-
$smartNull = $this->smartNullFrom($class);
229+
$smartNull = $this->smartNullFrom(SmartArrayHtml::class);
231230

232231
$trimmed = $smartNull->trim();
233232
$this->assertInstanceOf(SmartString::class, $trimmed);
@@ -238,17 +237,32 @@ public function testSmartStringOnlyMethodsDelegateToANullSmartString(string $cla
238237
$this->assertSame('n/a', $fallback->value());
239238
}
240239

241-
#[DataProvider('modeProvider')]
242-
public function testSmartStringTypeCastsReturnEmptyScalars(string $class): void
240+
public function testSmartStringTypeCastsReturnEmptyScalarsInHtmlMode(): void
243241
{
244-
$smartNull = $this->smartNullFrom($class);
242+
$smartNull = $this->smartNullFrom(SmartArrayHtml::class);
245243

246244
$this->assertSame('', $smartNull->string());
247245
$this->assertSame(0, $smartNull->int());
248246
$this->assertSame(0.0, $smartNull->float());
249247
$this->assertFalse($smartNull->bool());
250248
}
251249

250+
public function testSmartStringMethodsThrowInRawMode(): void
251+
{
252+
// Raw values are plain scalars with no methods, so a miss answers
253+
// SmartString calls with the same Error as any unknown method
254+
$smartNull = $this->smartNullFrom(SmartArray::class);
255+
256+
foreach (['trim', 'or', 'string'] as $method) {
257+
try {
258+
$smartNull->$method('n/a');
259+
$this->fail("expected an Error from ->$method()");
260+
} catch (Error $e) {
261+
$this->assertStringStartsWith("Call to undefined method SmartArray->$method(), ", $e->getMessage());
262+
}
263+
}
264+
}
265+
252266
//endregion
253267
//region Method delegation: unknown methods
254268

0 commit comments

Comments
 (0)