Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move RecursiveDataStructureTraverser to wp-cli/wp-cli package #5864

Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions php/WP_CLI/Exceptions/NonExistentKeyException.php
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 php/WP_CLI/Traversers/RecursiveDataStructureTraverser.php
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 tests/WP_CLI/Traversers/RecursiveDataStructureTraverserTest.php
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.' );
}
}