Skip to content

Instantly share code, notes, and snippets.

@Warbo
Last active August 29, 2015 14:01
Show Gist options
  • Save Warbo/9d8425fcdd7c026c795a to your computer and use it in GitHub Desktop.
Save Warbo/9d8425fcdd7c026c795a to your computer and use it in GitHub Desktop.
Array/object lookup function, especially for Drupal
<?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;
}
}
@JGrubb
Copy link

JGrubb commented May 6, 2014

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment