Skip to content

Commit 693636e

Browse files
Add configurable missing-key behavior (silent, warning, exception) (#56)
* Missing key behavior Add configurable missing-key behavior (silent, warning, exception) * Update CHANGELOG.md * PR Review
1 parent 03dd7f2 commit 693636e

4 files changed

Lines changed: 176 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 1.2.1 - WIP
4+
- Added configurable missing-key behavior: silent, warning, or exception.
5+
36
## 1.2.0 - 2026-01-08
47
- Removing PestPHP, using phpunit 11
58
- Support from PHP 8.2 to 8.5

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,60 @@ For example, `$data->getBlock("avocado.color")` returns a Block object with just
423423

424424
If you are going to access a non-valid key, an empty Block object is returned, so the `$data->getBlock("avocado.notexists")` returns a Block object with a length equal to 0.
425425

426+
### Missing key behavior
427+
428+
By default, accessing a non-existing key returns the provided default value **silently**.
429+
430+
You can configure three behaviors:
431+
432+
- **Silent** (default)
433+
- **Warning** (non-fatal)
434+
- **Exception**
435+
436+
437+
#### Silent (default)
438+
439+
```php
440+
$fruits = Block::make($fruitsArray);
441+
// OR if you want to be more explicit: $fruits = Block::make($fruitsArray)->silentOnMissingKey();
442+
443+
$nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // no warning, no exception
444+
```
445+
446+
#### Warning
447+
448+
```php
449+
$fruits = Block::make($fruitsArray)->warnOnMissingKey();
450+
451+
$nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // PHP warning
452+
```
453+
454+
#### Exception
455+
456+
```php
457+
$fruits = Block::make($fruitsArray)
458+
->throwOnMissingKey();
459+
460+
$nothing = $fruits->get("a-missing-key"); // throws exception
461+
462+
463+
$fruits->throwOnMissingKey(
464+
MyMissingKeyException::class,
465+
"The key in the configuration JSON file doens't exist?"
466+
);
467+
```
468+
469+
You can also pass your own exception class (must extend `\Throwable`).
470+
471+
#### Summary for the "missing key behavior"
472+
473+
| Mode | Result |
474+
| --------- | ------------------------------------ |
475+
| Silent (default behavior) | Returns default value |
476+
| Warning | Emits warning, returns default value |
477+
| Exception | Throws exception |
478+
479+
426480
### The `set()` method
427481
The `set()` method supports keys with the dot (or custom) notation for setting values for nested data.
428482
If a key doesn't exist, the `set()` method creates one and sets the value.

src/Block.php

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,119 @@ final class Block implements Iterator, ArrayAccess, Countable
3838
/** @var array<int|string, mixed> */
3939
private array $data;
4040

41+
/**
42+
* Missing key handling mode: return default value silently.
43+
*/
44+
private const MISSING_KEY_SILENT = 0;
45+
46+
/**
47+
* Missing key handling mode: emit a warning and continue execution.
48+
*/
49+
private const MISSING_KEY_WARNING = 1;
50+
51+
/**
52+
* Missing key handling mode: throw an exception.
53+
*/
54+
private const MISSING_KEY_EXCEPTION = 2;
55+
56+
/**
57+
* Current missing key handling mode.
58+
*
59+
* One of:
60+
* - self::MISSING_KEY_SILENT
61+
* - self::MISSING_KEY_WARNING
62+
* - self::MISSING_KEY_EXCEPTION
63+
*/
64+
private int $missingKeyMode = self::MISSING_KEY_SILENT;
65+
66+
67+
/** @var class-string<\Throwable>|null Exception class to throw on missing key */
68+
private ?string $missingKeyExceptionClass = null;
69+
/** @var string|null Optional hint appended to the exception message */
70+
private ?string $missingKeyExceptionHint = null;
71+
4172
/** @param array<int|string, mixed> $data */
4273
public function __construct(array $data = [], private bool $iteratorReturnsBlock = true)
4374
{
4475
$this->data = $data;
4576
}
4677

78+
/**
79+
* Use silent mode when accessing missing keys (return default value).
80+
*/
81+
public function silentOnMissingKey(): self
82+
{
83+
$this->missingKeyMode = self::MISSING_KEY_SILENT;
84+
$this->missingKeyExceptionClass = null;
85+
$this->missingKeyExceptionHint = null;
86+
return $this;
87+
}
88+
89+
/**
90+
* Emit a warning when accessing a missing key.
91+
*/
92+
public function warnOnMissingKey(): self
93+
{
94+
$this->missingKeyMode = self::MISSING_KEY_WARNING;
95+
96+
return $this;
97+
}
98+
99+
/**
100+
* Configure the Block object to throw an exception when accessing a missing key.
101+
*
102+
* When enabled, any access to a non-existing key will throw an exception of the
103+
* given class instead of returning the default value.
104+
*
105+
* An optional hint can be provided to add extra context to the exception message.
106+
*
107+
* @param class-string<\Throwable> $exceptionClass Exception class to throw (must extend Throwable)
108+
* @param string|null $hint Optional additional context appended to the exception message
109+
*
110+
* @return self Returns the current instance for method chaining.
111+
*
112+
* @throws \InvalidArgumentException If the given class does not extend Throwable
113+
*/
114+
public function throwOnMissingKey(
115+
string $exceptionClass = \OutOfBoundsException::class,
116+
?string $hint = null,
117+
): self {
118+
/** @phpstan-ignore function.alreadyNarrowedType, booleanAnd.alwaysFalse */
119+
if (!is_subclass_of($exceptionClass, \Throwable::class) && $exceptionClass !== \Throwable::class) {
120+
throw new \InvalidArgumentException("Exception class must extend Throwable");
121+
}
122+
123+
$this->missingKeyMode = self::MISSING_KEY_EXCEPTION;
124+
$this->missingKeyExceptionClass = $exceptionClass;
125+
$this->missingKeyExceptionHint = $hint;
126+
127+
return $this;
128+
}
129+
130+
private function handleMissingKey(int|string $key, mixed $defaultValue): mixed
131+
{
132+
switch ($this->missingKeyMode) {
133+
case self::MISSING_KEY_WARNING:
134+
trigger_error("Undefined array key: " . $key, E_USER_WARNING);
135+
return $defaultValue;
136+
137+
case self::MISSING_KEY_EXCEPTION:
138+
$class = $this->missingKeyExceptionClass ?? \OutOfBoundsException::class;
139+
$message = "Undefined array key: " . $key;
140+
if ($this->missingKeyExceptionHint) {
141+
$message = $message . " (" . $this->missingKeyExceptionHint . ")";
142+
}
143+
144+
throw new $class($message);
145+
146+
case self::MISSING_KEY_SILENT:
147+
default:
148+
return $defaultValue;
149+
}
150+
}
151+
152+
153+
47154
public function iterateBlock(bool $returnsBlock = true): self
48155
{
49156
$this->iteratorReturnsBlock = $returnsBlock;
@@ -89,13 +196,17 @@ public function get(int|string $key, mixed $defaultValue = null, string $charNes
89196
} elseif ($nestedValue instanceof Block) {
90197
$nestedValue = $nestedValue->get($nestedKey);
91198
} else {
92-
return $defaultValue;
199+
return $this->handleMissingKey($key, $defaultValue);
93200
}
94201
}
95202
return $nestedValue;
96203
}
97204
}
98-
return $this->data[$key] ?? $defaultValue;
205+
if (!array_key_exists($key, $this->data)) {
206+
return $this->handleMissingKey($key, $defaultValue);
207+
}
208+
209+
return $this->data[$key];
99210
}
100211

101212

@@ -247,9 +358,4 @@ public function applyField(
247358
$this->set($targetKey, $callable($this->get($key)));
248359
return $this;
249360
}
250-
251-
252-
253-
254-
255361
}

tests/BlockJsonTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,10 @@ public function testSaveToJsonWithExistingFile(): void
109109

110110
unlink("fruits.json");
111111
}
112+
113+
public function testEmptyJson(): void
114+
{
115+
$data = Block::fromJsonFile("file-not-exists");
116+
$this->assertSame(0, $data->count());
117+
}
112118
}

0 commit comments

Comments
 (0)