I had to come up with a way to convert array keys using undescores (underscore_case) into camelCase. This had to be done recursively since I did not know what arrays will be
I can quickly spot two separate tasks. One is to convert strings to camel-case format, and the other is to map the keys of a multi-dimensional array. These tasks have nothing to do with the other, and so it is best to implement them as separate functions.
Lets start with a higher-order function mapArrayKeys
. It will accept a mapping function and apply this function to each key of the array, producing a new array. We must expect the mapping function to be injective (one-to-one).
function mapArrayKeys(callable $f, array $xs) {
$out = array();
foreach ($xs as $key => $value) {
$out[$f($key)] = is_array($value) ? mapArrayKeys($f, $value) : $value;
}
return $out;
}
There are a few fiddly bits that I do not consider so important. You may not want to do type hinting on the parameters, okay fine. Maybe you'd rather if/then/else instead of a ternary operator, okay fine. What is important is that with mapArrayKeys
you can apply any (injective) mapping function to array keys.
The second task is to convert strings to camel-case. You might use PCRE functions for this, that's fine. I am going to use explode
to do the splitting.
function underToCamel($str) {
return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
}
Now these two functions can be used in tandem to achieve the overall goal of converting array keys from underscore to camel-case format.
mapArrayKeys('underToCamel', array('foo_bar' => array ('baz_qux' => 0)));
A note on injectivity. The function underToCamel
is not necessarily injective, so you have to take special care. You have to assume that for all x_y
and all xY
(where Y is the capitalized version of y) that exactly one of x_y
, xY
, x_Y
is an underscore format (same follows more more underscores).
So for example, underToCamel("foo_bar") == "fooBar"
and underToCamel("fooBar") == "fooBar"
and underToCamel("foo_Bar") == "fooBar"
and therefore only one can be a valid underscore format.
This is in response to a comment by luqo33.
What I meant by 'too complex' (at least in my view) is that this solution uses a lot of nested functions (e.g. four functions called in underToCamel, all nested - hinders readibility).
The line of code in question is this.
lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
I contest that this is readable. I do acknowledge that this style is not typical for PHP, and I think that is why the PHP reader may be put off.
First it should be noted that nested functions are not actually as abnormal as you'd think. Consider a math expression.
(-$b + sqrt($b*$b - 4*$a*$c)) / (2*$a)
This is an expression that uses a lot of nested functions: +, -, *, /
. If you pretend you haven't had BEDMAS (or equivalent) embedded into your subconscious, this is actually a complicated expression to understand -- there are implicit rules you are subconsciously applying to know that first you do the stuff in parentheses, then the multiplications, and so on. None of this seems complicated because you have learned how to read such expressions and it is now part of your repertoire. The same goes for reading expressions like the one I used.
I can rewrite the expression so that there is one function used per line.
$a = explode('_', $str);
$b = array_map('ucfirst', $a);
$c = implode('', $b);
$d = lcfirst($c);
Now the execution order is read top-to-bottom. I can also write it to read bottom-to-top.
lcfirst(
implode('',
array_map('ucfirst',
explode('_',
$str
))));
Lastly I can write it to read right-to-left or inside-to-out (if you consider the parentheses), which is how it was originally written.
lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
All of these versions use a simple pattern called function composition, which is another reason it is easy to read and understand. With function composition you can build up a sequence of functions where each function feeds from the output of the previous function.
To explain this scenario, my sequence of functions in left-to-right order is explode '_', array_map 'ucfirst', implode '', lcfirst
. The way it works can be clearly seem from the version which uses the variables $a
through $d
. You throw something into explode '_'
, and the result from that is passed into array_map 'ucfirst'
, and then into implode ''
, and finally into lcfirst
. You could think of this as a pipeline, or an assembly line, or something like that.