From 5e727a32e65aeea1e4346a0cad57d5a168f373a8 Mon Sep 17 00:00:00 2001 From: Roberto Butti Date: Sun, 11 Jan 2026 09:38:02 +0100 Subject: [PATCH 1/3] Missing key behavior Add configurable missing-key behavior (silent, warning, exception) --- README.md | 48 +++++++++++++++++++++++++++ src/Block.php | 73 +++++++++++++++++++++++++++++++++++++---- tests/BlockJsonTest.php | 6 ++++ 3 files changed, 120 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7371022..f23f7db 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,54 @@ For example, `$data->getBlock("avocado.color")` returns a Block object with just 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. +### Missing key behavior + +By default, accessing a non-existing key returns the provided default value **silently**. + +You can configure three behaviors: + +- **Silent** (default) +- **Warning** (non-fatal) +- **Exception** + + +#### Silent (default) + +```php +$fruits = Block::make($fruitsArray); +// OR if you want to be more explicit: $fruits = Block::make($fruitsArray)->silentOnMissingKey(); + +$nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // no warning, no exception +``` + +#### Warning + +```php +$fruits = Block::make($fruitsArray)->warnOnMissingKey(); + +$nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // PHP warning +``` + +#### Exception + +```php +$fruits = Block::make($fruitsArray) + ->throwOnMissingKey(\OutOfBoundsException::class); + +$nothing = $fruits->get("a-missing-key"); // throws exception +``` + +You can also pass your own exception class (must extend `\Throwable`). + +#### Summary for the "missing key behavior" + +| Mode | Result | +| --------- | ------------------------------------ | +| Silent (default behavior) | Returns default value | +| Warning | Emits warning, returns default value | +| Exception | Throws exception | + + ### The `set()` method The `set()` method supports keys with the dot (or custom) notation for setting values for nested data. If a key doesn't exist, the `set()` method creates one and sets the value. diff --git a/src/Block.php b/src/Block.php index 7100f7a..66ffe35 100644 --- a/src/Block.php +++ b/src/Block.php @@ -38,12 +38,72 @@ final class Block implements Iterator, ArrayAccess, Countable /** @var array */ private array $data; + private const MISSING_KEY_SILENT = 0; + private const MISSING_KEY_WARNING = 1; + private const MISSING_KEY_EXCEPTION = 2; + + private int $missingKeyMode = self::MISSING_KEY_SILENT; + + /** @var class-string<\Throwable>|null */ + private ?string $missingKeyExceptionClass = null; + /** @param array $data */ public function __construct(array $data = [], private bool $iteratorReturnsBlock = true) { $this->data = $data; } + public function silentOnMissingKey(): self + { + $this->missingKeyMode = self::MISSING_KEY_SILENT; + return $this; + } + + public function warnOnMissingKey(bool $enabled = true): self + { + $this->missingKeyMode = $enabled + ? self::MISSING_KEY_WARNING + : self::MISSING_KEY_SILENT; + + return $this; + } + + /** + * + * @param class-string<\Throwable> $exceptionClass + */ + public function throwOnMissingKey(string $exceptionClass = \OutOfBoundsException::class): self + { + /** @phpstan-ignore function.alreadyNarrowedType, booleanAnd.alwaysFalse */ + if (!is_subclass_of($exceptionClass, \Throwable::class) && $exceptionClass !== \Throwable::class) { + throw new \InvalidArgumentException("Exception class must extend Throwable"); + } + + $this->missingKeyMode = self::MISSING_KEY_EXCEPTION; + $this->missingKeyExceptionClass = $exceptionClass; + + return $this; + } + + private function handleMissingKey(int|string $key, mixed $defaultValue): mixed + { + switch ($this->missingKeyMode) { + case self::MISSING_KEY_WARNING: + trigger_error("Undefined array key: " . $key, E_USER_WARNING); + return $defaultValue; + + case self::MISSING_KEY_EXCEPTION: + $class = $this->missingKeyExceptionClass ?? \OutOfBoundsException::class; + throw new $class("Undefined array key: " . $key); + + case self::MISSING_KEY_SILENT: + default: + return $defaultValue; + } + } + + + public function iterateBlock(bool $returnsBlock = true): self { $this->iteratorReturnsBlock = $returnsBlock; @@ -89,13 +149,17 @@ public function get(int|string $key, mixed $defaultValue = null, string $charNes } elseif ($nestedValue instanceof Block) { $nestedValue = $nestedValue->get($nestedKey); } else { - return $defaultValue; + return $this->handleMissingKey($key, $defaultValue); } } return $nestedValue; } } - return $this->data[$key] ?? $defaultValue; + if (!array_key_exists($key, $this->data)) { + return $this->handleMissingKey($key, $defaultValue); + } + + return $this->data[$key]; } @@ -247,9 +311,4 @@ public function applyField( $this->set($targetKey, $callable($this->get($key))); return $this; } - - - - - } diff --git a/tests/BlockJsonTest.php b/tests/BlockJsonTest.php index f87a435..d42c3fc 100644 --- a/tests/BlockJsonTest.php +++ b/tests/BlockJsonTest.php @@ -109,4 +109,10 @@ public function testSaveToJsonWithExistingFile(): void unlink("fruits.json"); } + + public function testEmptyJson(): void + { + $data = Block::fromJsonFile("file-not-exists"); + $this->assertSame(0, $data->count()); + } } From 640bbddd78c3ac7b5836757a609d75ee2cd3026c Mon Sep 17 00:00:00 2001 From: Roberto Butti Date: Sun, 11 Jan 2026 09:41:58 +0100 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccbd994..a7ccccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 1.2.1 - WIP +- Added configurable missing-key behavior: silent, warning, or exception. + ## 1.2.0 - 2026-01-08 - Removing PestPHP, using phpunit 11 - Support from PHP 8.2 to 8.5 From 17e2e0ba03990713e8f60ce0fe78512380703a8a Mon Sep 17 00:00:00 2001 From: Roberto Butti Date: Sun, 11 Jan 2026 18:38:14 +0100 Subject: [PATCH 3/3] PR Review --- README.md | 8 +++++- src/Block.php | 69 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f23f7db..3ddfd12 100644 --- a/README.md +++ b/README.md @@ -455,9 +455,15 @@ $nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // PHP warning ```php $fruits = Block::make($fruitsArray) - ->throwOnMissingKey(\OutOfBoundsException::class); + ->throwOnMissingKey(); $nothing = $fruits->get("a-missing-key"); // throws exception + + +$fruits->throwOnMissingKey( + MyMissingKeyException::class, + "The key in the configuration JSON file doens't exist?" +); ``` You can also pass your own exception class (must extend `\Throwable`). diff --git a/src/Block.php b/src/Block.php index 66ffe35..638635a 100644 --- a/src/Block.php +++ b/src/Block.php @@ -38,14 +38,36 @@ final class Block implements Iterator, ArrayAccess, Countable /** @var array */ private array $data; - private const MISSING_KEY_SILENT = 0; - private const MISSING_KEY_WARNING = 1; + /** + * Missing key handling mode: return default value silently. + */ + private const MISSING_KEY_SILENT = 0; + + /** + * Missing key handling mode: emit a warning and continue execution. + */ + private const MISSING_KEY_WARNING = 1; + + /** + * Missing key handling mode: throw an exception. + */ private const MISSING_KEY_EXCEPTION = 2; + /** + * Current missing key handling mode. + * + * One of: + * - self::MISSING_KEY_SILENT + * - self::MISSING_KEY_WARNING + * - self::MISSING_KEY_EXCEPTION + */ private int $missingKeyMode = self::MISSING_KEY_SILENT; - /** @var class-string<\Throwable>|null */ + + /** @var class-string<\Throwable>|null Exception class to throw on missing key */ private ?string $missingKeyExceptionClass = null; + /** @var string|null Optional hint appended to the exception message */ + private ?string $missingKeyExceptionHint = null; /** @param array $data */ public function __construct(array $data = [], private bool $iteratorReturnsBlock = true) @@ -53,27 +75,46 @@ public function __construct(array $data = [], private bool $iteratorReturnsBlock $this->data = $data; } + /** + * Use silent mode when accessing missing keys (return default value). + */ public function silentOnMissingKey(): self { $this->missingKeyMode = self::MISSING_KEY_SILENT; + $this->missingKeyExceptionClass = null; + $this->missingKeyExceptionHint = null; return $this; } - public function warnOnMissingKey(bool $enabled = true): self + /** + * Emit a warning when accessing a missing key. + */ + public function warnOnMissingKey(): self { - $this->missingKeyMode = $enabled - ? self::MISSING_KEY_WARNING - : self::MISSING_KEY_SILENT; + $this->missingKeyMode = self::MISSING_KEY_WARNING; return $this; } /** + * Configure the Block object to throw an exception when accessing a missing key. + * + * When enabled, any access to a non-existing key will throw an exception of the + * given class instead of returning the default value. + * + * An optional hint can be provided to add extra context to the exception message. * - * @param class-string<\Throwable> $exceptionClass + * @param class-string<\Throwable> $exceptionClass Exception class to throw (must extend Throwable) + * @param string|null $hint Optional additional context appended to the exception message + * + * @return self Returns the current instance for method chaining. + * + * @throws \InvalidArgumentException If the given class does not extend Throwable */ - public function throwOnMissingKey(string $exceptionClass = \OutOfBoundsException::class): self - { + public function throwOnMissingKey( + string $exceptionClass = \OutOfBoundsException::class, + ?string $hint = null, + ): self { /** @phpstan-ignore function.alreadyNarrowedType, booleanAnd.alwaysFalse */ if (!is_subclass_of($exceptionClass, \Throwable::class) && $exceptionClass !== \Throwable::class) { throw new \InvalidArgumentException("Exception class must extend Throwable"); @@ -81,6 +122,7 @@ public function throwOnMissingKey(string $exceptionClass = \OutOfBoundsException $this->missingKeyMode = self::MISSING_KEY_EXCEPTION; $this->missingKeyExceptionClass = $exceptionClass; + $this->missingKeyExceptionHint = $hint; return $this; } @@ -94,7 +136,12 @@ private function handleMissingKey(int|string $key, mixed $defaultValue): mixed case self::MISSING_KEY_EXCEPTION: $class = $this->missingKeyExceptionClass ?? \OutOfBoundsException::class; - throw new $class("Undefined array key: " . $key); + $message = "Undefined array key: " . $key; + if ($this->missingKeyExceptionHint) { + $message = $message . " (" . $this->missingKeyExceptionHint . ")"; + } + + throw new $class($message); case self::MISSING_KEY_SILENT: default: