Last active
August 29, 2015 14:01
-
-
Save Warbo/9d8425fcdd7c026c795a to your computer and use it in GitHub Desktop.
Array/object lookup function, especially for Drupal
This file contains hidden or 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
<?php | |
/** | |
* Looks up values from deep inside a container (object or array) in a safe way. | |
* For example: | |
* | |
* lookup( | |
* array('foo' => array('bar' => 'baz')), | |
* array('foo', 'bar') | |
* ) === 'baz' | |
* | |
* @param mixed Array or object to look up value from. | |
* @param mixed A "path" of indices to drill down through the container. | |
* Either a single string/int, or an array of them. | |
* @param mixed A value, if one is found, or else NULL. | |
*/ | |
function lookup($container, $path) { | |
$path = is_array($path)? $path : array($path); | |
while (TRUE) { | |
// Result found, return it | |
if (count($path) === 0) return $container; | |
$key = reset($path); | |
if (is_array($container) && | |
(is_int($key) || is_string($key)) && | |
array_key_exists($key, $container)) { | |
// Recurse: lookup($container[$key], array_slice($path, 1)) | |
// PHP doesn't have tail-call optimisation, so we do it manually | |
$container = $container[$key]; | |
$path = array_slice($path, 1); // count($path) approaches 0 | |
continue; | |
} | |
if (is_object($container) && | |
is_string($key)) { | |
// We need to handle magic __get methods, so there's no point testing | |
// whether property_exists($container, $key). This gives a NOTICE on error | |
// so we need to use an error handler. We *also* need to use try/catch, | |
// since some important Drupal objects throw exceptions from their __get. | |
$handler = set_error_handler( | |
function($severity, $message, $filename, $lineno) use (&$container) { | |
$container = NULL; | |
return TRUE; | |
}, | |
E_ALL); | |
try { | |
$container = $container->$key; | |
} catch (EntityMetadataWrapperException $e) { | |
if (strpos($e->getMessage(), 'Unknown data property ') !== 0) { | |
throw $e; | |
} | |
$container = NULL; | |
} | |
set_error_handler($handler); | |
if (!is_null($container)) { | |
// Recurse: lookup($container->$key, array_slice($path, 1)) | |
// PHP doesn't have tail-call optimisation, so we do it manually | |
$path = array_slice($path, 1); // count($path) approaches 0 | |
continue; | |
} | |
} | |
// Not found, abort safely | |
return NULL; | |
} | |
} | |
// Test class. NOTE: This has been teased out of an existing hierarchy, it may need patching up | |
class ArrayTests extends DrupalUnitTestCase { | |
protected static $description = 'Useful array-handling functions'; | |
protected function testLookupFindsValuesInArrays() { | |
// Create a nested array (tree) with a known path to a leaf | |
// Initialise everything, wrapping a known value | |
$value = $this->randomString(); | |
$key = $this->randomString(); | |
$path = array($key); | |
$arr = array($key => $value); | |
// Now bury the value in random layers of cruft | |
for ($i = mt_rand(0, 10); $i; --$i) { | |
for ($j = mt_rand(10, 20); $j; --$j) { | |
$arr[$this->randomString()] = $this->randomString(); | |
} | |
$key = $this->randomString(); | |
$path[] = $key; | |
$arr = array($key => $arr); | |
} | |
// To get the value out, we need to go back down the path | |
$path = array_reverse($path); | |
$this->assertIdentical(lookup($arr, $path), | |
$value, | |
'Looked up value in an array'); | |
} | |
protected function testLookupHandlesMissingIndices() { | |
$arr = array( | |
$this->randomString() => array( | |
$this->randomString() => $this->randomString())); | |
$this->assertNull( | |
lookup($arr, array(array_rand($arr) + 1, mt_rand(0, 255))), | |
'Cannot look up values which do not exist'); | |
} | |
protected function testLookupFindsValuesInObjects() { | |
// Initialise everything, wrapping a known value | |
$value = $this->randomString(); | |
$key = $this->randomString(); | |
$path = array($key); | |
$obj = new stdClass; | |
$obj->$key = $value; | |
// Now bury the value in random layers of cruft | |
for ($i = mt_rand(0, 10); $i; --$i) { | |
for ($j = mt_rand(10, 20); $j; --$j) { | |
$obj->{$this->randomString()} = $this->randomString(); | |
} | |
$key = $this->randomString(); | |
$path[] = $key; | |
$tmp = new stdClass; | |
$tmp->$key = $obj; | |
$obj = $tmp; | |
} | |
// To get the value out, we need to go back down the path | |
$path = array_reverse($path); | |
$this->assertIdentical(lookup($obj, $path), | |
$value, | |
'Looked up value in an object'); | |
} | |
protected function testLookupHandlesMissingProperties() { | |
$this->assertNull(lookup(new stdClass, array($this->randomString())), | |
'Cannot look up properties which do not exist'); | |
} | |
protected function testLookupDoesNotOverflowTheStack() { | |
$value = mt_rand(1000, 9999); | |
$key = $this->randomName(); | |
$heavily_nested = array($key => $value); | |
$depth = mt_rand(500, 2000); | |
for ($i = $depth; $i; --$i) { | |
if (mt_rand(0, 1) === 0) { | |
$heavily_nested = array($key => $heavily_nested); | |
} | |
else { | |
$temp = new stdClass; | |
$temp->$key = $heavily_nested; | |
$heavily_nested = $temp; | |
unset($temp); | |
} | |
} | |
$this->assertIdentical(lookup($heavily_nested, | |
array_fill(0, $depth + 1, $key)), | |
$value, | |
'lookup handles deep collections'); | |
} | |
protected function testLookupFollowsMagicMethods() { | |
// Classes are global in PHP so we use a ridiculous name to avoid conflicts. | |
// Class declarations also can't be nested for some reason, so we're forced | |
// to use eval. | |
eval(<<<'EOF' | |
class TestLookupFollowsMagicMethodsClass { | |
public function __get($name) { | |
if ($name === 'thisShouldWork') { | |
return mt_rand(1, 100); | |
} | |
$trace = debug_backtrace(); | |
trigger_error('Undefined property via __get(): ' . $name . | |
' in ' . $trace[0]['file'] . | |
' on line ' . $trace[0]['line'], | |
E_USER_NOTICE); | |
return null; | |
} | |
} | |
EOF | |
); | |
$this->assertTrue( | |
is_numeric(lookup( | |
new TestLookupFollowsMagicMethodsClass, | |
array('thisShouldWork'))), | |
'lookup found magic __get value'); | |
} | |
protected function testLookupAllowsUnboxedName() { | |
$name = $this->randomString(); | |
$value = mt_rand(0, 1000); | |
$this->assertIdentical(lookup(array($name => $value), $name), | |
$value, | |
'Can lookup single names directly'); | |
} | |
// Helper functions | |
/** | |
* @return array A random array of int => int | |
*/ | |
protected function randomArray() { | |
$result = array(); | |
for ($i = mt_rand(0, 10); $i; --$i) { | |
$result[mt_rand()] = mt_rand(); | |
} | |
return $result; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you!