Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
73 changes: 66 additions & 7 deletions src/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,72 @@ final class Block implements Iterator, ArrayAccess, Countable
/** @var array<int|string, mixed> */
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<int|string, mixed> $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;
Expand Down Expand Up @@ -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];
}


Expand Down Expand Up @@ -247,9 +311,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());
}
}