Skip to content

Commit

Permalink
RecursiveItems::advanceToKey() chaining + array access
Browse files Browse the repository at this point in the history
  • Loading branch information
halaxa committed Nov 24, 2024
1 parent 4275f18 commit 81db7bc
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 12 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## master
### Added
- Recursive iteration via new facade `RecursiveItems`.
- Recursive iteration via new facade `RecursiveItems`. See **Recursive iteration** in README.

<br>

Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ for PHP >=7.2. See [TL;DR](#tl-dr). No dependencies in production except optiona
[![Latest Stable Version](https://img.shields.io/github/v/release/halaxa/json-machine?color=blueviolet&include_prereleases&logoColor=white)](https://packagist.org/packages/halaxa/json-machine)
[![Monthly Downloads](https://img.shields.io/packagist/dt/halaxa/json-machine?color=%23f28d1a)](https://packagist.org/packages/halaxa/json-machine)

---
NEW in version `1.2.0` - [Recursive iteration](#recursive)

---

* [TL;DR](#tl-dr)
Expand Down Expand Up @@ -383,13 +386,18 @@ foreach ($users as $user) {
#### Convenience methods of `RecursiveItems`
- `toArray(): array`
If you are sure that a certain instance of RecursiveItems is pointing to a memory-manageable data structure
(for example, $friend), you can call `$friend->toArray()`, and the item will materialize into a PHP array.
(for example, $friend), you can call `$friend->toArray()`, and the item will materialize into a plain PHP array.

- `advanceToKey(int|string $key): scalar|RecursiveItems`
When searching for a specific key in a collection (for example, `$user["friends"]`),
When searching for a specific key in a collection (for example, `'friends'` in `$user`),
you do not need to use a loop and a condition to search for it.
Instead, you can simply call `$user->advanceToKey("friends")`.
It will iterate for you and return the value at this key.
It will iterate for you and return the value at this key. Calls can be chained.
It also supports **array like syntax** for advancing to and getting following indices.
So `$user['friends']` would be an alias for `$user->advanceToKey('friends')`. Calls can be chained.
Keep in min that it's just an alias - **you won't be able to random-access previous indices**
after using this directly on `RecursiveItems`. It's just a syntax sugar.
Use `toArray()` if you need random access to indices on a record/item.

The previous example could thus be simplified as follows:
```php
Expand All @@ -400,12 +408,22 @@ use JsonMachine\RecursiveItems
$users = RecursiveItems::fromFile('users.json');
foreach ($users as $user) {
/** @var $user RecursiveItems */
foreach ($user->advanceToKey('friends') as $friend) {
foreach ($user['friends'] as $friend) { // or $user->advanceToKey('friends')
/** @var $friend RecursiveItems */
$friendArray = $friend->toArray();
$friendArray['username'] == 'friend1';
$friendArray['username'] === 'friend1';
}
}
```
Chaining allows you to do something like this:
```php
<?php

use JsonMachine\RecursiveItems

$users = RecursiveItems::fromFile('users.json');
$users[0]['friends'][1]['username'] === 'friend2';

```

#### Also `RecursiveItems implements \RecursiveIterator`
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/BadMethodCallException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace JsonMachine\Exception;

class BadMethodCallException extends JsonMachineException
{
}
9 changes: 9 additions & 0 deletions src/Exception/OutOfBoundsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace JsonMachine\Exception;

class OutOfBoundsException extends JsonMachineException
{
}
54 changes: 49 additions & 5 deletions src/RecursiveItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
use Generator;
use Iterator;
use IteratorAggregate;
use JsonMachine\Exception\BadMethodCallException;
use JsonMachine\Exception\InvalidArgumentException;
use JsonMachine\Exception\JsonMachineException;
use JsonMachine\Exception\OutOfBoundsException;

/**
* Entry-point facade for recursive iteration.
*/
final class RecursiveItems implements \RecursiveIterator, PositionAware
final class RecursiveItems implements \RecursiveIterator, PositionAware, \ArrayAccess
{
use FacadeTrait;

Expand Down Expand Up @@ -100,7 +102,7 @@ public function current()
if ($current instanceof IteratorAggregate) {
return new self($current, $this->options);
} elseif ( ! is_scalar($current)) {
throw new JsonMachineException(
throw new InvalidArgumentException(
sprintf(
'%s only accepts scalar or IteratorAggregate values. %s given.',
self::class,
Expand Down Expand Up @@ -160,21 +162,21 @@ public function getChildren(): ?\RecursiveIterator
*
* @return scalar|self
*
* @throws JsonMachineException when the key is not found on this level
* @throws OutOfBoundsException when the key is not found on this level
*/
public function advanceToKey($key)
{
if ( ! $this->parserIterator) {
$this->rewind();
}
$iterator = $this->parserIterator;
$iterator = $this;

while ($key !== $iterator->key() && $iterator->valid()) {
$iterator->next();
}

if ($key !== $iterator->key()) {
throw new JsonMachineException("Key '$key' was not found.");
throw new OutOfBoundsException("Key '$key' was not found.");
}

return $iterator->current();
Expand Down Expand Up @@ -214,4 +216,46 @@ private static function toArrayRecursive(self $traversable): array

return $array;
}

public function offsetExists($offset): bool
{
try {
$this->advanceToKey($offset);

return true;
} catch (JsonMachineException $e) {
return false;
}
}

#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->advanceToKey($offset);
}

/**
* @param $offset
* @param $value
*
* @throws BadMethodCallException
*
* @deprecated
*/
public function offsetSet($offset, $value): void
{
throw new BadMethodCallException('Unsupported: Cannot set a value on a JSON stream');
}

/**
* @param $offset
*
* @throws BadMethodCallException
*
* @deprecated
*/
public function offsetUnset($offset): void
{
throw new BadMethodCallException('Unsupported: Cannot unset a value from a JSON stream');
}
}
61 changes: 60 additions & 1 deletion test/JsonMachineTest/RecursiveItemsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function testGetChildrenReturnsNestedIterator()
$this->assertSame(null, $result[2]);
}

public function testAdvanceToKeyWorks()
public function testAdvanceToKeyWorksOnScalars()
{
$generator = function () {yield from ['one' => 1, 'two' => 2, 'three' => 3]; };
$iterator = new RecursiveItems(toIteratorAggregate($generator()));
Expand All @@ -81,6 +81,24 @@ public function testAdvanceToKeyWorks()
$this->assertSame(3, $iterator->advanceToKey('three'));
}

public function testArrayAccessIsASyntaxSugarToAdvanceToKey()
{
$generator = function () {
yield 'one' => 1;
yield 'two' => 2;
yield 'three' => 3;
};
$iterator = new RecursiveItems(toIteratorAggregate($generator()));

$this->assertTrue(isset($iterator['two']));
$this->assertTrue(isset($iterator['two']));

$this->assertSame(2, $iterator['two']);
$this->assertSame(3, $iterator['three']);

$this->assertFalse(isset($iterator['four']));
}

public function testAdvanceToKeyThrows()
{
$generator = function () {yield from ['one' => 1, 'two' => 2, 'three' => 3]; };
Expand All @@ -90,6 +108,47 @@ public function testAdvanceToKeyThrows()
$iterator->advanceToKey('four');
}

public function testAdvanceToKeyCanBeChained()
{
$generator = function ($iterable) {yield from ['one' => 1, 'two' => 2, 'i' => $iterable, 'three' => 3]; };
$iterator = new RecursiveItems(
toIteratorAggregate($generator(
toIteratorAggregate($generator(
toIteratorAggregate(new \ArrayIterator(['42']))
))
))
);

$this->assertSame(
'42',
$iterator
->advanceToKey('i')
->advanceToKey('i')
->advanceToKey(0)
);
}

public function testAdvanceToKeyArraySyntaxCanBeChained()
{
$generator = function ($iterable) {yield from ['one' => 1, 'two' => 2, 'i' => $iterable, 'three' => 3]; };
$iterator = new RecursiveItems(
toIteratorAggregate($generator(
toIteratorAggregate($generator(
toIteratorAggregate(new \ArrayIterator(['42']))
))
))
);

$this->assertSame('42', $iterator['i']['i'][0]);
}

public function testAdvanceToKeyArraySyntaxCanBeChainedE2E()
{
$iterator = RecursiveItems::fromString('[[[42]]]');

$this->assertSame(42, $iterator[0][0][0]);
}

public function testToArray()
{
$generator = function ($iterable) {yield from ['one' => 1, 'two' => 2, 'i' => $iterable, 'three' => 3]; };
Expand Down

0 comments on commit 81db7bc

Please sign in to comment.