Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revert "Revert "Move
RecursiveDataStructureTraverser
to `wp-cli/wp-…
- Loading branch information
1 parent
00ca209
commit 132c69f
Showing
3 changed files
with
358 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
183
php/WP_CLI/Traversers/RecursiveDataStructureTraverser.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
151
tests/WP_CLI/Traversers/RecursiveDataStructureTraverserTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.' ); | ||
} | ||
} |