Skip to content

Commit

Permalink
Revert "Revert "Move RecursiveDataStructureTraverser to `wp-cli/wp-…
Browse files Browse the repository at this point in the history
…cli` package (#5864)" (#5866)"

This reverts commit a4303bc.
  • Loading branch information
schlessera committed Nov 14, 2023
1 parent 00ca209 commit 132c69f
Show file tree
Hide file tree
Showing 3 changed files with 358 additions and 0 deletions.
24 changes: 24 additions & 0 deletions php/WP_CLI/Exceptions/NonExistentKeyException.php
@@ -0,0 +1,24 @@
<?php

namespace WP_CLI\Exceptions;

use OutOfBoundsException;

class NonExistentKeyException extends OutOfBoundsException {
/** @var RecursiveDataStructureTraverser */
protected $traverser;

/**
* @param RecursiveDataStructureTraverser $traverser
*/
public function set_traverser( $traverser ) {
$this->traverser = $traverser;
}

/**
* @return RecursiveDataStructureTraverser
*/
public function get_traverser() {
return $this->traverser;
}
}
183 changes: 183 additions & 0 deletions php/WP_CLI/Traversers/RecursiveDataStructureTraverser.php
@@ -0,0 +1,183 @@
<?php

namespace WP_CLI\Traversers;

use UnexpectedValueException;
use WP_CLI\Exceptions\NonExistentKeyException;

class RecursiveDataStructureTraverser {

/**
* @var mixed The data to traverse set by reference.
*/
protected $data;

/**
* @var null|string The key the data belongs to in the parent's data.
*/
protected $key;

/**
* @var null|static The parent instance of the traverser.
*/
protected $parent;

/**
* RecursiveDataStructureTraverser constructor.
*
* @param mixed $data The data to read/manipulate by reference.
* @param string|int $key The key/property the data belongs to.
* @param static|null $parent_instance The parent instance of the traverser.
*/
public function __construct( &$data, $key = null, $parent_instance = null ) {
$this->data =& $data;
$this->key = $key;
$this->parent = $parent_instance;
}

/**
* Get the nested value at the given key path.
*
* @param string|int|array $key_path
*
* @return static
*/
public function get( $key_path ) {
return $this->traverse_to( (array) $key_path )->value();
}

/**
* Get the current data.
*
* @return mixed
*/
public function value() {
return $this->data;
}

/**
* Update a nested value at the given key path.
*
* @param string|int|array $key_path
* @param mixed $value
*/
public function update( $key_path, $value ) {
$this->traverse_to( (array) $key_path )->set_value( $value );
}

/**
* Update the current data with the given value.
*
* This will mutate the variable which was passed into the constructor
* as the data is set and traversed by reference.
*
* @param mixed $value
*/
public function set_value( $value ) {
$this->data = $value;
}

/**
* Unset the value at the given key path.
*
* @param $key_path
*/
public function delete( $key_path ) {
$this->traverse_to( (array) $key_path )->unset_on_parent();
}

/**
* Define a nested value while creating keys if they do not exist.
*
* @param array $key_path
* @param mixed $value
*/
public function insert( $key_path, $value ) {
try {
$this->update( $key_path, $value );
} catch ( NonExistentKeyException $exception ) {
$exception->get_traverser()->create_key();
$this->insert( $key_path, $value );
}
}

/**
* Delete the key on the parent's data that references this data.
*/
public function unset_on_parent() {
$this->parent->delete_by_key( $this->key );
}

/**
* Delete the given key from the data.
*
* @param $key
*/
public function delete_by_key( $key ) {
if ( is_array( $this->data ) ) {
unset( $this->data[ $key ] );
} else {
unset( $this->data->$key );
}
}

/**
* Get an instance of the traverser for the given hierarchical key.
*
* @param array $key_path Hierarchical key path within the current data to traverse to.
*
* @throws NonExistentKeyException
*
* @return static
*/
public function traverse_to( array $key_path ) {
$current = array_shift( $key_path );

if ( null === $current ) {
return $this;
}

if ( ! $this->exists( $current ) ) {
$exception = new NonExistentKeyException( "No data exists for key \"{$current}\"" );
$exception->set_traverser( new static( $this->data, $current, $this->parent ) );
throw $exception;
}

foreach ( $this->data as $key => &$key_data ) {
if ( $key === $current ) {
$traverser = new static( $key_data, $key, $this );
return $traverser->traverse_to( $key_path );
}
}
}

/**
* Create the key on the current data.
*
* @throws UnexpectedValueException
*/
protected function create_key() {
if ( is_array( $this->data ) ) {
$this->data[ $this->key ] = null;
} elseif ( is_object( $this->data ) ) {
$this->data->{$this->key} = null;
} else {
$type = gettype( $this->data );
throw new UnexpectedValueException(
"Cannot create key \"{$this->key}\" on data type {$type}"
);
}
}

/**
* Check if the given key exists on the current data.
*
* @param string $key
*
* @return bool
*/
public function exists( $key ) {
return ( is_array( $this->data ) && array_key_exists( $key, $this->data ) ) ||
( is_object( $this->data ) && property_exists( $this->data, $key ) );
}
}
151 changes: 151 additions & 0 deletions tests/WP_CLI/Traversers/RecursiveDataStructureTraverserTest.php
@@ -0,0 +1,151 @@
<?php

namespace WP_CLI\Tests\Traversers;

use WP_CLI\Tests\TestCase;
use WP_CLI\Traversers\RecursiveDataStructureTraverser;

class RecursiveDataStructureTraverserTest extends TestCase {

public function test_it_can_get_a_top_level_array_value() {
$array = array(
'foo' => 'bar',
);

$traverser = new RecursiveDataStructureTraverser( $array );

$this->assertEquals( 'bar', $traverser->get( 'foo' ) );
}

public function test_it_can_get_a_top_level_object_value() {
$object = (object) array(
'foo' => 'bar',
);

$traverser = new RecursiveDataStructureTraverser( $object );

$this->assertEquals( 'bar', $traverser->get( 'foo' ) );
}

public function test_it_can_get_a_nested_array_value() {
$array = array(
'foo' => array(
'bar' => array(
'baz' => 'value',
),
),
);

$traverser = new RecursiveDataStructureTraverser( $array );

$this->assertEquals( 'value', $traverser->get( array( 'foo', 'bar', 'baz' ) ) );
}

public function test_it_can_get_a_nested_object_value() {
$object = (object) array(
'foo' => (object) array(
'bar' => 'baz',
),
);

$traverser = new RecursiveDataStructureTraverser( $object );

$this->assertEquals( 'baz', $traverser->get( array( 'foo', 'bar' ) ) );
}

public function test_it_can_set_a_nested_array_value() {
$array = array(
'foo' => array(
'bar' => 'baz',
),
);
$this->assertEquals( 'baz', $array['foo']['bar'] );

$traverser = new RecursiveDataStructureTraverser( $array );
$traverser->update( array( 'foo', 'bar' ), 'new' );

$this->assertEquals( 'new', $array['foo']['bar'] );
}

public function test_it_can_set_a_nested_object_value() {
$object = (object) array(
'foo' => (object) array(
'bar' => 'baz',
),
);
$this->assertEquals( 'baz', $object->foo->bar );

$traverser = new RecursiveDataStructureTraverser( $object );
$traverser->update( array( 'foo', 'bar' ), 'new' );

$this->assertEquals( 'new', $object->foo->bar );
}

public function test_it_can_update_an_integer_object_value() {
$object = (object) array(
'test_mode' => 0,
);
$this->assertEquals( 0, $object->test_mode );

$traverser = new RecursiveDataStructureTraverser( $object );
$traverser->update( array( 'test_mode' ), 1 );

$this->assertEquals( 1, $object->test_mode );
}

public function test_it_can_delete_a_nested_array_value() {
$array = array(
'foo' => array(
'bar' => 'baz',
),
);
$this->assertArrayHasKey( 'bar', $array['foo'] );

$traverser = new RecursiveDataStructureTraverser( $array );
$traverser->delete( array( 'foo', 'bar' ) );

$this->assertArrayNotHasKey( 'bar', $array['foo'] );
}

public function test_it_can_delete_a_nested_object_value() {
$object = (object) array(
'foo' => (object) array(
'bar' => 'baz',
),
);
$this->assertObjectHasAttribute( 'bar', $object->foo );

$traverser = new RecursiveDataStructureTraverser( $object );
$traverser->delete( array( 'foo', 'bar' ) );

$this->assertObjectNotHasAttribute( 'bar', $object->foo );
}

public function test_it_can_insert_a_key_into_a_nested_array() {
$array = array(
'foo' => array(
'bar' => 'baz',
),
);

$traverser = new RecursiveDataStructureTraverser( $array );
$traverser->insert( array( 'foo', 'new' ), 'new value' );

$this->assertArrayHasKey( 'new', $array['foo'] );
$this->assertEquals( 'new value', $array['foo']['new'] );
}

public function test_it_throws_an_exception_when_attempting_to_create_a_key_on_an_invalid_type() {
$data = 'a string';
$traverser = new RecursiveDataStructureTraverser( $data );

try {
$traverser->insert( array( 'key' ), 'value' );
} catch ( \Exception $e ) {
$this->assertSame( 'a string', $data );
return;
}

$this->fail( 'Failed to assert that an exception was thrown when inserting a key into a string.' );
}
}

0 comments on commit 132c69f

Please sign in to comment.