Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,60 @@ 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();

$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`).

#### 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.
Expand Down
120 changes: 113 additions & 7 deletions src/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,119 @@ final class Block implements Iterator, ArrayAccess, Countable
/** @var array<int|string, mixed> */
private array $data;

/**
* 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 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<int|string, mixed> $data */
public function __construct(array $data = [], private bool $iteratorReturnsBlock = true)
{
$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;
}

/**
* Emit a warning when accessing a missing key.
*/
public function warnOnMissingKey(): self
{
$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 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,
?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");
}

$this->missingKeyMode = self::MISSING_KEY_EXCEPTION;
$this->missingKeyExceptionClass = $exceptionClass;
$this->missingKeyExceptionHint = $hint;

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;
$message = "Undefined array key: " . $key;
if ($this->missingKeyExceptionHint) {
$message = $message . " (" . $this->missingKeyExceptionHint . ")";
}

throw new $class($message);

case self::MISSING_KEY_SILENT:
default:
return $defaultValue;
}
}



public function iterateBlock(bool $returnsBlock = true): self
{
$this->iteratorReturnsBlock = $returnsBlock;
Expand Down Expand Up @@ -89,13 +196,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];
}


Expand Down Expand Up @@ -247,9 +358,4 @@ public function applyField(
$this->set($targetKey, $callable($this->get($key)));
return $this;
}





}
6 changes: 6 additions & 0 deletions tests/BlockJsonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}