6
\$\begingroup\$

I have this task where I have an associated array in PHP and I want to manipulate the order of entries. Basically I want to be able to move entry Y before entry X.

So basically if I have the following array:

['foo1' => 'bar', 'foo2' => 'buzz', 'foo3' => 'bazz']

I want to be able to say: 'put element foo3 before foo2', resulting in the following:

['foo1' => 'bar', 'foo3' => 'bazz', 'foo2' => 'buzz']

I currently have the following code:

/**
 * Change the order of an associated array, moving by key before another
 *
 * @param array $arr
 * @param string $key
 * @param string $before
 * @return array
 */
public function moveKeyBefore(array $arr, string $key, string $before)
{
    $keys = array_keys($arr);
    if (in_array($key, $keys) && in_array($before, $keys)) {
        $newArr = [];
        foreach ($arr as $idx => $value) {
            switch ($idx) {
                case $key :
                    continue;
                case $before:
                    $newArr[$key] = $arr[$key];
                    $newArr[$before] = $value;
                    break;
                default:
                    $newArr[$idx] = $value;
                    break;
            }
        }

        return $newArr;
    }

    return $arr;
}

This code basically works. However, I was wondering if there was an easier, less complex way of doing this.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Generally speaking, I think that you are thinking about associative (not "associated") arrays in the wrong manner. Normally, one does not use associative arrays when array element ordering needs to be guaranteed. This is what numerically-indexed arrays are for. It should be a big red flag to you that PHP has no built-in functions to deal with ordering of associative arrays like it does for numerically-indexed arrays.

Now, if you need to have key-value combinations like you present here and be able to render them in a given order, perhaps for when they are being iterated for output (which is really the only use case I can think of for why you would want to do this), you might consider one of two options.

First, you could create a numerically-indexed array which references the key value array, and use this for iteration.

For example:

$keyValueArray = ['foo1' => 'bar', 'foo2' => 'buzz', 'foo3' => 'bazz'];
$keyOrder = ['foo1', 'foo2', 'foo3'];

Now, if you want to change the "order" of your key-value pairs, you simply do it in the $keyOrder array. And then iterate out like this:

foreach($keyOrder as $key) {
    echo $keyValueArray[$key];
}

Alternately, you might find it more intuitive to build an array of objects (or array of associative arrays).

$yourArray = [
    (object) ['foo1' => 'bar'],
    (object) ['foo2' => 'buzz'],
    (object) ['foo3' => 'bazz']
];

function placeObjectBeforeByKey(array $arr, string $key, string $before)
{
    $keyIdx = null;
    $beforeIdx = null;
    foreach($arr as $idx => $obj) {
        if(isset($obj[$key])) {
            $keyIdx = $idx;
        }
        if(isset($obj[$before])) {
            $beforeIdx = $idx;
        }
        if(!is_null($keyIdx) && !is_null($beforeIdx)) {
            break;
        }
    }
    if(is_null($keyIdx) || is_null($beforeIdx)) {
        // bad input, perhaps throw exception or log error and return $arr unaltered
    }
    if($keyIdx === $beforeIdx) {
        // same object had matching keys for both $key and $before
        // perhaps throw exception or just return $arr unaltered
        return $arr;
    }
    $keySlice = array_splice($arr, $keyIdx, 1);
    if($keyIdx < $beforeIdx) $beforeIdx--;
    array_splice($arr, $beforeIdx, 0, $keySlice);
}

Note that either of these approaches, in essence, do the same as what you do in your function, when you are using array_keys() to build a new array from scratch. The main difference from your approach and the alternatives I propose is that you are being more transparent to other areas of code around how ordering of these underlying key-value pairs is achieved. To me, if I were a developer picking up your code base, and I saw that you were simply using an associative array, I would not expect this to imply any ordering, whereas if you gave me an array of objects/arrays or an array which describes ordering of an associative array, it would be very clear to me that there is intended ordering to these pairs.

\$\endgroup\$
3
  • \$\begingroup\$ '... perhaps for when they are being iterated for output (which is really the only use case I can think of for why you would want to do this) ...' This is actually exactly what my current situation is. Thanks for the new insight. I didn't even know you could cast arrays to objects like that. Learned something new today! :-) \$\endgroup\$ Jun 12, 2017 at 8:01
  • \$\begingroup\$ Normally, one does not use associative arrays when array element ordering needs to be guaranteed. This is what numerically-indexed arrays are for. there could be some cases to use this... eg when you use array_multisort, this method actually sorts by the first element. Lets say you have an assoc array, you may want to pic a element by name to be first because you dont know the position in array and can't identify it by its content because other element may have the same content... \$\endgroup\$
    – Dwza
    Mar 31, 2020 at 8:45
  • \$\begingroup\$ You are assuming that it's his choice to do this, but right now I'm working on rearranging breadcrumbs in Magento 2 and their devs have decided to use associative arrays as if the order is guaranteed. Don't forget that people aren't always writing code that's fully theirs. Of course I could change the way Magento handles breadcrumbs, but that would take way too long. \$\endgroup\$ May 24 at 10:39
6
\$\begingroup\$

I wouldn't bother with a foreach loop because it will do more iterating than necessary. I recommend halving the array at the desired position and squeezing the element-to-be-moved into the gap created. Consider this much more concise method:

Code: (Demo Link)

function moveKeyBefore($arr, $find, $move) {
    if (!isset($arr[$find], $arr[$move])) {
        return $arr;
    }

    $elem = [$move=>$arr[$move]];  // cache the element to be moved
    $start = array_splice($arr, 0, array_search($find, array_keys($arr)));
    unset($start[$move]);  // only important if $move is in $start
    return $start + $elem + $arr;
}
$arr = ['foo1'=>'bar', 'foo2'=>'buzz', 'foo3'=>'bazz'];
var_export(moveKeyBefore($arr, 'foo3', 'foo1'));

Output:

array (
  'foo2' => 'buzz',
  'foo1' => 'bar',
  'foo3' => 'bazz',
)

Explanation:

First check that both of the nominated keys exist in the array, if not return the input array untouched. isset() is very fast, certainly faster than in_array(). Furthermore, the !isset() check will require both keys to exist otherwise the early return will be triggered. This is as fast as this part gets.

Next, if both $find and $move exist in the array, my function will generate three array fragments:

  • $elem contains the associative array element declared by $move.
  • $start contains the elements that precede $find. array_search() returns the offset (numeric key) of the $find value in $arr.
  • $arr is modified by array_splice() and contains all elements from find to the end.

After the splice, when $move is in the $arr half, the union action bumps the duplicate element out. When $move is in the $start half, the convenient "bump" is lost, this means that the $move element needs to be removed from its original location. unset() can be used unconditionally because it doesn't error when the target element is not found.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.