9

On migrating to PHP 7.4 I have to deal with a different behavior of some array functions like reset(), current() or end() concerning ArrayObject. The following example produces different outputs:

<?php

$array = new \ArrayObject(["a", "b"]);
$item = end($array);
var_dump($item);


$array = ["a", "b"];
$item = end($array);
var_dump($item);

With php 7.4 the output is:

bool(false)
string(1) "b"

On PHP versions before 7.4 the output is the following:

string(1) "b"
string(1) "b"

A end($array->getArrayCopy()) produces a notice, but might be a workaround if used with a variable.

Is there a way to emulate the behavior of end() with an ArrayObject or ArrayIterator? The ArrayObject could be very big, an iteration to the end might not be the best solution.

Trendfischer
  • 7,112
  • 5
  • 40
  • 51
  • An alternative could be `$item = $array[count($array)-1];`. Not sure if that's the most efficient solution. – Patrick Q Feb 21 '20 at 13:09
  • 3
    I'd say that qualifies as a PHP bug, there's definitely nothing in the changelog that would suggest this was an intended change in 7.4 – iainn Feb 21 '20 at 13:11
  • Test it online: https://3v4l.org/4MADI – 0stone0 Feb 21 '20 at 13:11
  • As `ArrayObject` and `ArrayIterator` both implement `ArrayAccess` Interface, you can just typecast the `ArrayObject` (or w/e) to array and pass it to `end()`. Edit: Tested it in PHP 7.4.0 – Y.Hermes Feb 21 '20 at 13:12
  • 1
    @PatrickQ what if it's associative? – Andreas Feb 21 '20 at 13:14
  • 4
    @iainn this is definitely not a bug - https://www.php.net/manual/en/migration74.incompatible.php#migration74.incompatible.spl – u_mulder Feb 21 '20 at 13:15
  • [`end()`](https://www.php.net/manual/en/function.end.php) accepts an array and you are giving it an object. This doesn't throw any error as it seems. But [`array_pop()`](https://www.php.net/manual/en/function.array-pop.php) does throw an error. – nice_dev Feb 21 '20 at 13:16
  • @Andreas Then you ask a different question with the appropriate conditions of your problem :) You'll also not I didn't post it as an Answer. – Patrick Q Feb 21 '20 at 13:16
  • @u_mulder Well then I take that back. That'll teach me for looking through the changelog rather than the breaking changes. – iainn Feb 21 '20 at 13:17

4 Answers4

3

From PHP 7.4 array methods don't operate on internal array, but on ArrayObject itself. I summarized two solutions for that.

1. Getting internal array of object.

$array = new \ArrayObject(["a", "b"]);
$item = end($array->getArrayCopy());

2. Creating Facade of ArrayObject and adding custom method end() to upgraded class.

Jsowa
  • 9,104
  • 5
  • 56
  • 60
2

You can make the arrayobject an array to get the keys then use end on the keys to get the last key.

$array = new \ArrayObject(["a", "b"]);
$keys = array_keys((array)$array);
$end_key = end($keys);

var_dump($array[$end_key]);

It's not a pretty solution but it works.
I suggest you make it a function so that you can call it when needed.

https://3v4l.org/HTGYn

As a function:

function end_object($array){
    $keys = array_keys((array)$array);
    $end_key = end($keys);
    return $array[$end_key];
}


$array = new \ArrayObject(["a", "b"]);
$item = end_object($array);
var_dump($item);
Andreas
  • 23,610
  • 6
  • 30
  • 62
  • I dont see difference between both answers when I look at the result and usaqe in question. if difference explain please –  Feb 21 '20 at 13:20
  • The other answer uses an array and casts it to an object which is not what OP asked for. I use an arrayObject and cast it to array to get the keys from it. – Andreas Feb 21 '20 at 13:22
  • So, is it solution for this question ? Is there a way to emulate the behavior of end() with an ArrayObject or ArrayIterator ? –  Feb 21 '20 at 13:31
  • That is why I posted it as an answer. Check the link if you don't believe me... – Andreas Feb 21 '20 at 13:33
  • 1
    I've tested the `array_keys()` solution with https://3v4l.org/IaEMM/perf#output but it needed 20-30% more on memory compared with an `end()` on a simple `getArrayCopy()` https://3v4l.org/uYv59/perf#output – Trendfischer Feb 21 '20 at 13:34
  • @Trendfischer my understanding of the performance tab on 3v4l is that it's a guide at best. Sometimes completely wrong. But that is just how I interpret the values there. – Andreas Feb 21 '20 at 13:38
  • 1
    @Trendfischer If memory is the issue and if you want to use `end` only, then you can create a wrapper class which implements `ArrayAccess` and have an additional function which returns an `end` of the internal private array that would be operated. – nice_dev Feb 21 '20 at 13:48
  • 3
    Question: what is the purpose of `array_keys` ? why don't you just cast it directly `$arr = (array) $array` and then `$end = end($arr)` – Rain Feb 21 '20 at 18:52
0

A slightly faster approach without casting or using an iterator would be to not use the constructor in the first place, and instead use append method which will create an array itself and you can use end on that array later

$array = new \ArrayObject();
$array->append(["a", "b"]);
$item =  end($array[count($array) - 1]);
var_dump($item);

count($array) - 1 in case you append another array later, we make sure that $item is always the last element in the last appended array.

Rain
  • 3,416
  • 3
  • 24
  • 40
  • 1
    Thanks, the solution with `count()` might be helpful in some cases, but your example would not work for something like this `new \ArrayObject([123 => "a", 456 => "c"]);` – Trendfischer Feb 24 '20 at 09:19
  • @Trendfischer I know that's why I used `append` instead of the constructor, using append with your example will definitely work. `$array->append([123 => "a", 456 => "c"]` – Rain Feb 24 '20 at 09:22
  • @Trendfischer Please note `count` is not for the elements of your array it's for the multidimensional array that `append` will create. For your array we are using `end` as usual. – Rain Feb 24 '20 at 09:34
  • 1
    I appreciate the intention, but I usually do not use an ArrayObject as a simple replacement for an array. The example in the question is exemplary to show the problem. Although if I only use `append()`, I could use a `count()`, that is valid solution. That could work with `append('a')` and `append('b')`. The key would be to disallow associative arrays which is a possibility by extending ArrayObject. – Trendfischer Feb 24 '20 at 13:05
0

Came up with this approach.

Should perform better than any of the above.

  • No casting to another variable using memory and CPU
  • Only one internal iterator.
  • Only one new variable, and it's a reference to the item.
class MyClass extends ArrayObject {
    public function last() {
        foreach ($this as $entity) {}
        return $entity ?? null;
    }
}

$array = new \MyClass(["a", "b", "c"]);
$item = $array->last();
var_dump($item);
Leon
  • 181
  • 1
  • 3