Skip to content

Instantly share code, notes, and snippets.

@colinodell
Last active May 28, 2016 14:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save colinodell/2290afec67538d5f3fe64ed2d7644dc2 to your computer and use it in GitHub Desktop.
Save colinodell/2290afec67538d5f3fe64ed2d7644dc2 to your computer and use it in GitHub Desktop.
array_change_keys()

Problem

AFAIK, there are no core PHP methods which allow you to re-key an array.

Okay, that's not entirely true - there is array_change_key_case(), but whenever I need to change an array's keys it's never been this use case.

You could do it with a foreach loop:

$newArray = [];
foreach ($oldArray as $key => $value) {
    // TODO: $newKey = ...;
    $newArray[$newKey] = $value;
}

But that's not very clean (nor functional). Alternatively, you could do this:

$newArray = array_combine(
    array_map(function ($value) {
        // TODO: return new key here
    }, array_values($oldArray)),
    array_values($oldArray)
);

But that's way too much code for such a simple action. And it's not immediately clear what you're trying to do.

Proposal

I propose implementing a new function which behaves exactly like that long array_combine example above. Perhaps this new function might be called array_change_keys(). This would take two parameters:

  • An array you want to re-key
  • A callable which is executed for each element of the array

That callable would take one or two parameters:

  • The value of the current item
  • The old key of that item (not sure if this is really needed)

Whatever the callable return becomes the new key for that item. If that new key is not an int, PHP will try to cast to a string.

So it's sort of like array_map or array_walk, but it modifies the keys instead (and keeps the values the same).

Example

Let's say we want to take an array of some entities and re-key them by id:

array_change_keys($entities, function($value) { return $value->getId(); });

Or if we wanted to re-key an array of strings by their MD5 hash:

array_change_keys($strings, 'md5');

Unanswered Questions

  • Should this create a new array (like array_map) or modify the current one (like sort)?
    • I'm leaning towards the former option.
  • What should happen if the callable doesn't return a unique string or int?
    • Ex: returns null or object, throws an exception, or returns a duplicate key?
    • IMO it should behave identically to that long array_combine() example I showed earlier.
  • Is there any value in passing the current key as the second parameter to the callable?
    • I'm thinking no, since those values are being thrown out. If the need arises, this could always be added in a future PHP version.
  • Which parameter should come first - the array or the callable?
  • Is this proposal even a good idea?
    • I hope so :)
* {{{ proto array array_change_keys(array input, mixed callback)
Retuns an array with all keys modified by a callback */
PHP_FUNCTION(array_change_keys)
{
zval *array, *value, *params;
zval result;
zend_fcall_info fci;
zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
zend_ulong num_key;
zend_string *str_key;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "af", &array, &fci, &fci_cache) == FAILURE) {
return;
}
array_init_size(return_value, zend_hash_num_elements(Z_ARRVAL_P(array)));
params = (zval *)safe_emalloc(2, sizeof(zval), 0);
ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_key, str_key, value) {
fci.retval = &result;
fci.param_count = 2;
fci.params = params;
fci.no_separation = 0;
if (str_key) {
ZVAL_STRING(&params[0], zend_string_copy(str_key));
} else {
ZVAL_LONG(&params[0], num_key);
}
ZVAL_COPY(&params[1], value);
if (zend_call_function(&fci, &fci_cache) != SUCCESS || Z_TYPE(result) == IS_UNDEF) {
zval_dtor(return_value);
zval_ptr_dtor(&params[0]);
RETURN_NULL();
} else {
zval_ptr_dtor(&params[0]);
}
if (Z_TYPE(result) == IS_STRING) {
fprintf(stderr, "KEY RETURNED: %s\n", Z_STRVAL(result));
zend_hash_add_new(Z_ARRVAL_P(return_value), Z_STR(result), value);
} else if (Z_TYPE(result) == IS_LONG) {
zend_hash_index_add_new(Z_ARRVAL_P(return_value), Z_LVAL(result), value);
} else {
php_error_docref(NULL, E_WARNING, "callback must return string or integer");
zval_dtor(return_value);
RETURN_NULL();
}
} ZEND_HASH_FOREACH_END();
}
/* }}} */
--TEST--
Test array_change_keys() function
--FILE--
<?php
$a = array('one', 'two', 'three', 'four');
// This re-keys the array by md5ing the values
var_dump( array_change_keys($a, function($k, $v) { return md5($v); }) );
// This re-keys the array by md5ing the keys
var_dump( array_change_keys($a, 'md5') );
?>
--EXPECTF--
array(4) {
["f97c5d29941bfb1b2fdab0874906ab82"]=>
string(3) "one"
["b8a9f715dbb64fd5c56e7783c6820a61"]=>
string(3) "two"
["35d6d33467aae9a2e3dccb4b6b027878"]=>
string(5) "three"
["8cbad96aced40b3838dd9f07f6ef5772"]=>
string(4) "four"
}
array(4) {
["cfcd208495d565ef66e7dff9f98764da"]=>
string(3) "one"
["c4ca4238a0b923820dcc509a6f75849b"]=>
string(3) "two"
["c81e728d9d4c2f636f067f89cc14862c"]=>
string(5) "three"
["eccbc87e4b5ce2fe28308fd9f2a7baf3"]=>
string(4) "four"
}
@SammyK
Copy link

SammyK commented May 27, 2016

@colinodell
Copy link
Author

Define global function in ext/standard/basic_functions.c - three changes:

  1. Add to zend_begin_arg_info_x
  2. php_fe
  3. mshutdown submodule macro things

@colinodell
Copy link
Author

If doing this as a separate file:

  • ext/standard/config.m4 - for non-Windows

  • ext/standard/config.w32 - for Windows

    look for a list of .c files and add your stuff to the end

@colinodell
Copy link
Author

create a .phpt test

run make test TESTS=[some path to file] to run them

@SammyK
Copy link

SammyK commented May 27, 2016

@jmikola
Copy link

jmikola commented May 27, 2016

Currently has some memory corruption, but this is something pieced together from array_map and array_change_key_case:

* {{{ proto array array_change_keys(array input, mixed callback)
   Retuns an array with all keys modified by a callback */
PHP_FUNCTION(array_change_keys)
{
    zval *array, *value, *params;
    zval result;
    zend_fcall_info fci;
    zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
    zend_ulong num_key;
    zend_string *str_key;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "af", &array, &fci, &fci_cache) == FAILURE) {
        return;
    }

    array_init_size(return_value, zend_hash_num_elements(Z_ARRVAL_P(array)));
    params = (zval *)safe_emalloc(2, sizeof(zval), 0);

    ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_key, str_key, value) {
        fci.retval = &result;
        fci.param_count = 2;
        fci.params = params;
        fci.no_separation = 0;

        if (str_key) {
            ZVAL_STRING(&params[0], zend_string_copy(str_key));
        } else {
            ZVAL_LONG(&params[0], num_key);
        }
        ZVAL_COPY(&params[1], value);

        if (zend_call_function(&fci, &fci_cache) != SUCCESS || Z_TYPE(result) == IS_UNDEF) {
            zval_dtor(return_value);
            zval_ptr_dtor(&params[0]);
            RETURN_NULL();
        } else {
            zval_ptr_dtor(&params[0]);
        }

        if (Z_TYPE(result) == IS_STRING) {
            fprintf(stderr, "KEY RETURNED: %s\n", Z_STRVAL(result));
            zend_hash_add_new(Z_ARRVAL_P(return_value), Z_STR(result), value);
        } else if (Z_TYPE(result) == IS_LONG) {
            zend_hash_index_add_new(Z_ARRVAL_P(return_value), Z_LVAL(result), value);
        } else {
            php_error_docref(NULL, E_WARNING, "callback must return string or integer");
            zval_dtor(return_value);
            RETURN_NULL();
        }
    } ZEND_HASH_FOREACH_END();
}
/* }}} */

@colinodell
Copy link
Author

I've updated the gist with the code above.

@colinodell
Copy link
Author

So ZVAL_STRING(&params[0], zend_string_copy(str_key)); doesn't seem to work - if the key is a string, you end up with this strange STX byte being passed into the callback. For example, if you do this:

$a = ['foo' => 'bar'];
array_change_keys($a, function($k) {
    var_dump($k);
    return $k;
});

The output would not be string(3) foo but rather string(1) STX (where STX is Sublime Text's representation of some single byte value).

Using ZVAL_STR_COPY or ZVAL_STR instead of ZVAL_STRING seems to fix this, though I'm not sure which one is more appropriate (or if either negates the need for the zend_string_copy() call).

Thoughts?

@colinodell
Copy link
Author

Fixed a couple more bugs - latest version can be found here: colinodell/php-src@master...rfc/array-change-keys

Also started drafting the RFC: https://wiki.php.net/rfc/array_change_keys

I'll work on writing some comprehensive tests next.

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