Skip to content

Commit f375042

Browse files
authored
Merge pull request #5871 from wp-cli/fix/reintroduce-recursive-traverser
2 parents 00ca209 + 6079dc4 commit f375042

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace WP_CLI\Exception;
4+
5+
use OutOfBoundsException;
6+
7+
class NonExistentKeyException extends OutOfBoundsException {
8+
/** @var RecursiveDataStructureTraverser */
9+
protected $traverser;
10+
11+
/**
12+
* @param RecursiveDataStructureTraverser $traverser
13+
*/
14+
public function set_traverser( $traverser ) {
15+
$this->traverser = $traverser;
16+
}
17+
18+
/**
19+
* @return RecursiveDataStructureTraverser
20+
*/
21+
public function get_traverser() {
22+
return $this->traverser;
23+
}
24+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
namespace WP_CLI\Traverser;
4+
5+
use UnexpectedValueException;
6+
use WP_CLI\Exception\NonExistentKeyException;
7+
8+
class RecursiveDataStructureTraverser {
9+
10+
/**
11+
* @var mixed The data to traverse set by reference.
12+
*/
13+
protected $data;
14+
15+
/**
16+
* @var null|string The key the data belongs to in the parent's data.
17+
*/
18+
protected $key;
19+
20+
/**
21+
* @var null|static The parent instance of the traverser.
22+
*/
23+
protected $parent;
24+
25+
/**
26+
* RecursiveDataStructureTraverser constructor.
27+
*
28+
* @param mixed $data The data to read/manipulate by reference.
29+
* @param string|int $key The key/property the data belongs to.
30+
* @param static|null $parent_instance The parent instance of the traverser.
31+
*/
32+
public function __construct( &$data, $key = null, $parent_instance = null ) {
33+
$this->data =& $data;
34+
$this->key = $key;
35+
$this->parent = $parent_instance;
36+
}
37+
38+
/**
39+
* Get the nested value at the given key path.
40+
*
41+
* @param string|int|array $key_path
42+
*
43+
* @return static
44+
*/
45+
public function get( $key_path ) {
46+
return $this->traverse_to( (array) $key_path )->value();
47+
}
48+
49+
/**
50+
* Get the current data.
51+
*
52+
* @return mixed
53+
*/
54+
public function value() {
55+
return $this->data;
56+
}
57+
58+
/**
59+
* Update a nested value at the given key path.
60+
*
61+
* @param string|int|array $key_path
62+
* @param mixed $value
63+
*/
64+
public function update( $key_path, $value ) {
65+
$this->traverse_to( (array) $key_path )->set_value( $value );
66+
}
67+
68+
/**
69+
* Update the current data with the given value.
70+
*
71+
* This will mutate the variable which was passed into the constructor
72+
* as the data is set and traversed by reference.
73+
*
74+
* @param mixed $value
75+
*/
76+
public function set_value( $value ) {
77+
$this->data = $value;
78+
}
79+
80+
/**
81+
* Unset the value at the given key path.
82+
*
83+
* @param $key_path
84+
*/
85+
public function delete( $key_path ) {
86+
$this->traverse_to( (array) $key_path )->unset_on_parent();
87+
}
88+
89+
/**
90+
* Define a nested value while creating keys if they do not exist.
91+
*
92+
* @param array $key_path
93+
* @param mixed $value
94+
*/
95+
public function insert( $key_path, $value ) {
96+
try {
97+
$this->update( $key_path, $value );
98+
} catch ( NonExistentKeyException $exception ) {
99+
$exception->get_traverser()->create_key();
100+
$this->insert( $key_path, $value );
101+
}
102+
}
103+
104+
/**
105+
* Delete the key on the parent's data that references this data.
106+
*/
107+
public function unset_on_parent() {
108+
$this->parent->delete_by_key( $this->key );
109+
}
110+
111+
/**
112+
* Delete the given key from the data.
113+
*
114+
* @param $key
115+
*/
116+
public function delete_by_key( $key ) {
117+
if ( is_array( $this->data ) ) {
118+
unset( $this->data[ $key ] );
119+
} else {
120+
unset( $this->data->$key );
121+
}
122+
}
123+
124+
/**
125+
* Get an instance of the traverser for the given hierarchical key.
126+
*
127+
* @param array $key_path Hierarchical key path within the current data to traverse to.
128+
*
129+
* @throws NonExistentKeyException
130+
*
131+
* @return static
132+
*/
133+
public function traverse_to( array $key_path ) {
134+
$current = array_shift( $key_path );
135+
136+
if ( null === $current ) {
137+
return $this;
138+
}
139+
140+
if ( ! $this->exists( $current ) ) {
141+
$exception = new NonExistentKeyException( "No data exists for key \"{$current}\"" );
142+
$exception->set_traverser( new static( $this->data, $current, $this->parent ) );
143+
throw $exception;
144+
}
145+
146+
foreach ( $this->data as $key => &$key_data ) {
147+
if ( $key === $current ) {
148+
$traverser = new static( $key_data, $key, $this );
149+
return $traverser->traverse_to( $key_path );
150+
}
151+
}
152+
}
153+
154+
/**
155+
* Create the key on the current data.
156+
*
157+
* @throws UnexpectedValueException
158+
*/
159+
protected function create_key() {
160+
if ( is_array( $this->data ) ) {
161+
$this->data[ $this->key ] = null;
162+
} elseif ( is_object( $this->data ) ) {
163+
$this->data->{$this->key} = null;
164+
} else {
165+
$type = gettype( $this->data );
166+
throw new UnexpectedValueException(
167+
"Cannot create key \"{$this->key}\" on data type {$type}"
168+
);
169+
}
170+
}
171+
172+
/**
173+
* Check if the given key exists on the current data.
174+
*
175+
* @param string $key
176+
*
177+
* @return bool
178+
*/
179+
public function exists( $key ) {
180+
return ( is_array( $this->data ) && array_key_exists( $key, $this->data ) ) ||
181+
( is_object( $this->data ) && property_exists( $this->data, $key ) );
182+
}
183+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace WP_CLI\Tests\Traverser;
4+
5+
use WP_CLI\Tests\TestCase;
6+
use WP_CLI\Traverser\RecursiveDataStructureTraverser;
7+
8+
class RecursiveDataStructureTraverserTest extends TestCase {
9+
10+
public function test_it_can_get_a_top_level_array_value() {
11+
$array = array(
12+
'foo' => 'bar',
13+
);
14+
15+
$traverser = new RecursiveDataStructureTraverser( $array );
16+
17+
$this->assertEquals( 'bar', $traverser->get( 'foo' ) );
18+
}
19+
20+
public function test_it_can_get_a_top_level_object_value() {
21+
$object = (object) array(
22+
'foo' => 'bar',
23+
);
24+
25+
$traverser = new RecursiveDataStructureTraverser( $object );
26+
27+
$this->assertEquals( 'bar', $traverser->get( 'foo' ) );
28+
}
29+
30+
public function test_it_can_get_a_nested_array_value() {
31+
$array = array(
32+
'foo' => array(
33+
'bar' => array(
34+
'baz' => 'value',
35+
),
36+
),
37+
);
38+
39+
$traverser = new RecursiveDataStructureTraverser( $array );
40+
41+
$this->assertEquals( 'value', $traverser->get( array( 'foo', 'bar', 'baz' ) ) );
42+
}
43+
44+
public function test_it_can_get_a_nested_object_value() {
45+
$object = (object) array(
46+
'foo' => (object) array(
47+
'bar' => 'baz',
48+
),
49+
);
50+
51+
$traverser = new RecursiveDataStructureTraverser( $object );
52+
53+
$this->assertEquals( 'baz', $traverser->get( array( 'foo', 'bar' ) ) );
54+
}
55+
56+
public function test_it_can_set_a_nested_array_value() {
57+
$array = array(
58+
'foo' => array(
59+
'bar' => 'baz',
60+
),
61+
);
62+
$this->assertEquals( 'baz', $array['foo']['bar'] );
63+
64+
$traverser = new RecursiveDataStructureTraverser( $array );
65+
$traverser->update( array( 'foo', 'bar' ), 'new' );
66+
67+
$this->assertEquals( 'new', $array['foo']['bar'] );
68+
}
69+
70+
public function test_it_can_set_a_nested_object_value() {
71+
$object = (object) array(
72+
'foo' => (object) array(
73+
'bar' => 'baz',
74+
),
75+
);
76+
$this->assertEquals( 'baz', $object->foo->bar );
77+
78+
$traverser = new RecursiveDataStructureTraverser( $object );
79+
$traverser->update( array( 'foo', 'bar' ), 'new' );
80+
81+
$this->assertEquals( 'new', $object->foo->bar );
82+
}
83+
84+
public function test_it_can_update_an_integer_object_value() {
85+
$object = (object) array(
86+
'test_mode' => 0,
87+
);
88+
$this->assertEquals( 0, $object->test_mode );
89+
90+
$traverser = new RecursiveDataStructureTraverser( $object );
91+
$traverser->update( array( 'test_mode' ), 1 );
92+
93+
$this->assertEquals( 1, $object->test_mode );
94+
}
95+
96+
public function test_it_can_delete_a_nested_array_value() {
97+
$array = array(
98+
'foo' => array(
99+
'bar' => 'baz',
100+
),
101+
);
102+
$this->assertArrayHasKey( 'bar', $array['foo'] );
103+
104+
$traverser = new RecursiveDataStructureTraverser( $array );
105+
$traverser->delete( array( 'foo', 'bar' ) );
106+
107+
$this->assertArrayNotHasKey( 'bar', $array['foo'] );
108+
}
109+
110+
public function test_it_can_delete_a_nested_object_value() {
111+
$object = (object) array(
112+
'foo' => (object) array(
113+
'bar' => 'baz',
114+
),
115+
);
116+
$this->assertObjectHasProperty( 'bar', $object->foo );
117+
118+
$traverser = new RecursiveDataStructureTraverser( $object );
119+
$traverser->delete( array( 'foo', 'bar' ) );
120+
121+
$this->assertObjectNotHasProperty( 'bar', $object->foo );
122+
}
123+
124+
public function test_it_can_insert_a_key_into_a_nested_array() {
125+
$array = array(
126+
'foo' => array(
127+
'bar' => 'baz',
128+
),
129+
);
130+
131+
$traverser = new RecursiveDataStructureTraverser( $array );
132+
$traverser->insert( array( 'foo', 'new' ), 'new value' );
133+
134+
$this->assertArrayHasKey( 'new', $array['foo'] );
135+
$this->assertEquals( 'new value', $array['foo']['new'] );
136+
}
137+
138+
public function test_it_throws_an_exception_when_attempting_to_create_a_key_on_an_invalid_type() {
139+
$data = 'a string';
140+
$traverser = new RecursiveDataStructureTraverser( $data );
141+
142+
try {
143+
$traverser->insert( array( 'key' ), 'value' );
144+
} catch ( \Exception $e ) {
145+
$this->assertSame( 'a string', $data );
146+
return;
147+
}
148+
149+
$this->fail( 'Failed to assert that an exception was thrown when inserting a key into a string.' );
150+
}
151+
}

0 commit comments

Comments
 (0)