Skip to content

Commit 5e727a3

Browse files
committed
Missing key behavior
Add configurable missing-key behavior (silent, warning, exception)
1 parent 9c238ff commit 5e727a3

3 files changed

Lines changed: 120 additions & 7 deletions

File tree

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,54 @@ 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(\OutOfBoundsException::class);
459+
460+
$nothing = $fruits->get("a-missing-key"); // throws exception
461+
```
462+
463+
You can also pass your own exception class (must extend `\Throwable`).
464+
465+
#### Summary for the "missing key behavior"
466+
467+
| Mode | Result |
468+
| --------- | ------------------------------------ |
469+
| Silent (default behavior) | Returns default value |
470+
| Warning | Emits warning, returns default value |
471+
| Exception | Throws exception |
472+
473+
426474
### The `set()` method
427475
The `set()` method supports keys with the dot (or custom) notation for setting values for nested data.
428476
If a key doesn't exist, the `set()` method creates one and sets the value.

src/Block.php

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

41+
private const MISSING_KEY_SILENT = 0;
42+
private const MISSING_KEY_WARNING = 1;
43+
private const MISSING_KEY_EXCEPTION = 2;
44+
45+
private int $missingKeyMode = self::MISSING_KEY_SILENT;
46+
47+
/** @var class-string<\Throwable>|null */
48+
private ?string $missingKeyExceptionClass = null;
49+
4150
/** @param array<int|string, mixed> $data */
4251
public function __construct(array $data = [], private bool $iteratorReturnsBlock = true)
4352
{
4453
$this->data = $data;
4554
}
4655

56+
public function silentOnMissingKey(): self
57+
{
58+
$this->missingKeyMode = self::MISSING_KEY_SILENT;
59+
return $this;
60+
}
61+
62+
public function warnOnMissingKey(bool $enabled = true): self
63+
{
64+
$this->missingKeyMode = $enabled
65+
? self::MISSING_KEY_WARNING
66+
: self::MISSING_KEY_SILENT;
67+
68+
return $this;
69+
}
70+
71+
/**
72+
*
73+
* @param class-string<\Throwable> $exceptionClass
74+
*/
75+
public function throwOnMissingKey(string $exceptionClass = \OutOfBoundsException::class): self
76+
{
77+
/** @phpstan-ignore function.alreadyNarrowedType, booleanAnd.alwaysFalse */
78+
if (!is_subclass_of($exceptionClass, \Throwable::class) && $exceptionClass !== \Throwable::class) {
79+
throw new \InvalidArgumentException("Exception class must extend Throwable");
80+
}
81+
82+
$this->missingKeyMode = self::MISSING_KEY_EXCEPTION;
83+
$this->missingKeyExceptionClass = $exceptionClass;
84+
85+
return $this;
86+
}
87+
88+
private function handleMissingKey(int|string $key, mixed $defaultValue): mixed
89+
{
90+
switch ($this->missingKeyMode) {
91+
case self::MISSING_KEY_WARNING:
92+
trigger_error("Undefined array key: " . $key, E_USER_WARNING);
93+
return $defaultValue;
94+
95+
case self::MISSING_KEY_EXCEPTION:
96+
$class = $this->missingKeyExceptionClass ?? \OutOfBoundsException::class;
97+
throw new $class("Undefined array key: " . $key);
98+
99+
case self::MISSING_KEY_SILENT:
100+
default:
101+
return $defaultValue;
102+
}
103+
}
104+
105+
106+
47107
public function iterateBlock(bool $returnsBlock = true): self
48108
{
49109
$this->iteratorReturnsBlock = $returnsBlock;
@@ -89,13 +149,17 @@ public function get(int|string $key, mixed $defaultValue = null, string $charNes
89149
} elseif ($nestedValue instanceof Block) {
90150
$nestedValue = $nestedValue->get($nestedKey);
91151
} else {
92-
return $defaultValue;
152+
return $this->handleMissingKey($key, $defaultValue);
93153
}
94154
}
95155
return $nestedValue;
96156
}
97157
}
98-
return $this->data[$key] ?? $defaultValue;
158+
if (!array_key_exists($key, $this->data)) {
159+
return $this->handleMissingKey($key, $defaultValue);
160+
}
161+
162+
return $this->data[$key];
99163
}
100164

101165

@@ -247,9 +311,4 @@ public function applyField(
247311
$this->set($targetKey, $callable($this->get($key)));
248312
return $this;
249313
}
250-
251-
252-
253-
254-
255314
}

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)