Converting keys of an array/object-tree to lowercase
Asked Answered
C

7

3

I am currently optimizing a PHP application and found one function being called around 10-20k times, so I'd thought I'd start optimization there:

function keysToLower($obj)
{
    if (!is_object($obj) && !is_array($obj))
        return $obj;

    foreach ($obj as $key => $element) {
        $element = keysToLower($element);
        if (is_object($obj)) {
            $obj->{strtolower($key)} = $element;
            if (!ctype_lower($key))
                unset($obj->{$key});
        } elseif (is_array($obj) && ctype_upper($key)) {
           $obj[strtolower($key)] = $element;
           unset($obj[$key]);
        }
    }
    return $obj;
}

Most of the time is spent in recursive calls (which are quite slow in PHP), but I don't see any way to convert it to a loop. How can I do this?

Calefactory answered 5/6, 2010 at 18:7 Comment(3)
You can always easily remove recursive calls using an auxiliary stack.Caboodle
I suggested array_walk_recursive but deleted my post -- I couldn't easily make it do what you wanted, although you may want to look into that function yourself.Oversold
Apparently array_walk_recursive won't consider elements that are created with the callback function.Calefactory
P
5

Foreach is using an internal copy that is then traversed. Try it without:

function keysToLower($obj)
{
    $type = (int) is_object($obj) - (int) is_array($obj);
    if ($type === 0) return $obj;
    reset($obj);
    while (($key = key($obj)) !== null)
    {
        $element = keysToLower(current($obj));
        switch ($type)
        {
        case 1:
            if (!is_int($key) && $key !== ($keyLowercase = strtolower($key)))
            {
                unset($obj->{$key});
                $key = $keyLowercase;
            }
            $obj->{$key} = $element;
            break;
        case -1:
            if (!is_int($key) && $key !== ($keyLowercase = strtolower($key)))
            {
                unset($obj[$key]);
                $key = $keyLowercase;
            }
            $obj[$key] = $element;
            break;
        }
        next($obj);
    }
    return $obj;
}

Or use references to avoid that a copy is used:

function &keysToLower(&$obj)
{
    $type = (int) is_object($obj) - (int) is_array($obj);
    if ($type === 0) return $obj;
    foreach ($obj as $key => &$val)
    {
        $element = keysToLower($val);
        switch ($type)
        {
        case 1:
            if (!is_int($key) && $key !== ($keyLowercase = strtolower($key)))
            {
                unset($obj->{$key});
                $key = $keyLowercase;
            }
            $obj->{$key} = $element;
            break;
        case -1:
            if (!is_int($key) && $key !== ($keyLowercase = strtolower($key)))
            {
                unset($obj[$key]);
                $key = $keyLowercase;
            }
            $obj[$key] = $element;
            break;
        }
    }
    return $obj;
}
Pendulous answered 5/6, 2010 at 18:27 Comment(7)
Works fine except with nested arrays without keys every array on the second level is empty, e.g. keysToLower(array(array(array(1,2)))) returns array(0=>array())Calefactory
And the if($key !== $keyLowercase) prevents values in an array with a lowercase key from being processed, e.g. array('lowercase'=>array('UPPERCASE'=>1)) won't work. Inserting an else $val=keysToLower($val); fixes this.Calefactory
The problem with the empty arrays can be fixed by changing if ($key !== $keyLowercase) to if ($key !== $keyLowercase && !ctype_digit($keyLowercase).Calefactory
I just checked, it runs about 30% faster than my version, moving the is_int in the switch statement and unsetting the old key first slows it down again.Calefactory
@tstenner: Which one did you use?Pendulous
The second with the references, I'll include it in the question.Calefactory
Creating a new object instead of unsetting the old keys speeds it up in my case.Calefactory
D
3

You might also want to lookup array_change_key_case().

For objects, you can do:

($obj)array_change_key_case((arr)$o)

Delaunay answered 10/8, 2012 at 4:50 Comment(1)
This is only suitable for arrays, not objects.Ettieettinger
G
2

I assume you don't care about casting to array...

function keys_to_lower($o) {
    if (is_object($o)) {
        $o = (array)$o;
    }
    if (is_array($o)) {
        return array_map('keys_to_lower', array_change_key_case($o));
    }
    else {
        return $o;
    }
}
Gorlin answered 5/6, 2010 at 19:54 Comment(1)
I do care, but I will measure, whether doing this and recasting it back before returning will make any difference.Calefactory
G
2

here a example using lambda:

$multiArrayChangeKeyCase = function (&$array) use (&$multiArrayChangeKeyCase) {
    $array = array_change_key_case($array);

    foreach ($array as $key => $row)
        if (is_array($row))
             $multiArrayChangeKeyCase($array[$key]);
};
Gnatcatcher answered 17/7, 2012 at 19:37 Comment(1)
This answer only solves half the asked question. This does not attempt to process object properties.Ettieettinger
E
1
array_combine(array_map("strtolower", array_keys($a)), array_values($a))
Endomorph answered 5/6, 2010 at 18:21 Comment(3)
Doesn't recurse, but probably a good start to speed up his codeOversold
Correction: works with arrays, as long as you cast them using (array) $aCalefactory
This is not suitable for the asked question.Ettieettinger
F
0

A some what late response to a old thread but, there's a native function that does this, you could wrap it up something along these lines.

function setKeyCasing($thing, $case = CASE_LOWER) {
    return array_change_key_case((array) $thing, $case);
}
Flynn answered 27/6, 2019 at 12:49 Comment(1)
This does not satisfy the requirements of the asked question.Ettieettinger
E
0

Here is a recursive function which modifies by reference to replace all keys/properties in an array or object structure that may contain any nested arrays or objects. It makes a copy of each level and overwrites the level after it is finished changing all keys.

Code: (Demo)

function allKeysToLower(array|object &$data): void
{
    $type = gettype($data);
    foreach ($data as $k => &$v) {
        if (is_array($v) || is_object($v)) {
            (__FUNCTION__)($v);  // go to deeper level
        }
        if (is_string($k)) {
            $k = strtolower($k);  // mutate the key
        }
        switch ($type) {
            case 'object':
                $new ??= (object) [];  // create object if not created, to allow population
                $new->{$k} = $v;  // add property to object
                break;
            case 'array':
                $new[$k] = $v;  // add element to array
        }
    }
    $data = $new ?? $data;  // fallback to empty $data if loop not entered
}

allKeysToLower($test);
var_export($test);
Ettieettinger answered 24/1 at 1:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.