Under the Radar? A Backwards-Compatible Break for SPLFixedArray in PHP 8

The official release date for PHP 8 is 26th November, just 9 days away, so we’re almost into the last week; and I’ve just discovered a change to SPLFixedArray that I wasn’t previously aware about. While not many developers use SPL Datastructures, and probably fewer still use SPLFixedArray, it is a big BC break; and it wasn’t something that I had seen documented as a PHP 8 change.

I have a small library for Benford distributions that uses SPLFixedArray (Benford distributions work with values of a predictable integer range, 10 values for a 1-digit distribution, 100 for a 2-digit distribution, so they’re a prime candidate to use SPLFixedArray where I can easily pre-allocate the required size to create a dataset for the full range of values). But because I want to treat the keys as strings with leading zero padding, and default the empty values to zero, and because SPLFixedArray implements Iterator, I’ve extended that class and overridden the current() and key() methods.


class Distribution extends SPLFixedArray {
    private $keyMask;

    public function __construct(int $digits = 1) {
        parent::__construct(10 ** $digits);
        $this->keyMask = "%0{$digits}s";
    }

    public function current(): int {
        return parent::current() ?? 0;
    }

    public function key(): string {
        return sprintf($this->keyMask, parent::key());
    }
}

It’s straightforward to run:


$distribution = new Distribution(1);

foreach ($distribution as $key => $value) {
    var_dump($key, $value);
}

and (until release 8.0.0 beta4) it gave me the expected results:


string(1) "0"
int(0)
string(1) "1"
int(0)
...
string(1) "9"
int(0)

string keys, and zero values for an unpopulated distribution.

However, yesterday I tried running this same code against PHP 8.0.0 rc3, and the result wasn’t what I expected.


int(0)
NULL
int(1)
NULL
int(2)
...
int(9)
NULL

integer keys, and null values for an unpopulated distribution. Exactly what I’d expect for an SPLFixedArray; but not for an Iterator (or any class that implements Iterator) when I’m overloading key() and current() specifically to return string keys and zero values. 😦

I tried a variation on my calling code, explicitly executing the appropriate Iterator methods to execute as a while loop:


$distribution->rewind();
while ($distribution->valid()) {
    $key = $distribution->key();
    $value = $distribution->current();
    var_dump($key, $value);
    $distribution->next();
}

And PHP 8.0.0 rc3 responded with:


Fatal error: Uncaught Error: Call to undefined method Distribution::rewind()

Definitely not what I would expect when working with an Iterator!!!


One quick check with a highly knowledgeable member of the core development team (Nikita Popov) later, and I had my answer:

SplFixedArray is now IteratorAggregate to support nested loops.

That certainly explained the problem: IteratorAggregate provides all the methods that are necessary to make an object Traversable; but they’re not publicly accessible. They can’t be called manually, and can’t be overloaded by extending a class that implements IteratorAggregate.

A little searching finally discovered a reference to the change in the PHP NEWS from 1st October, the change was actually introduced in rc1.


- SPL:
  - SplFixedArray is now IteratorAggregate rather than Iterator. (alexdowad)

and from UPGRADING:


- SPL:
  - SplFixedArray is now an IteratorAggregate and not an Iterator.
    SplFixedArray::rewind(), ::current(), ::key(), ::next(), and ::valid()
    have been removed. In their place, SplFixedArray::getIterator() has been
    added. Any code which uses explicit iteration over SplFixedArray must now
    obtain an Iterator through SplFixedArray::getIterator(). This means that
    SplFixedArray is now safe to use in nested loops.

I hadn’t seen any RFC for this change, and it had almost slipped by “under the radar”.


So what do I need to do to allow my Benford distributions library to work with PHP 8?

SPLFixedArray still implements ArrayAccess and Countable, so I can wrap the SPLFixedArray in an Iterator and manage my own index, still overload key() and current() to return the values that I want, and use ArrayAccess to retrieve the values when they are needed, and Countable to test the valid range. It just means that I need a bit more boilerplate code wrapped round the SPLFixedArray.


class Distribution implements Iterator {
    private $keyMask;
    private $values;
    private $index;

    public function __construct(int $digits = 1) {
        $this->values = new SPLFixedArray(10 ** $digits);
        $this->keyMask = "%0{$digits}s";
        $this->index = 0;
    }

    public function rewind(): void {
        $this->index = 0;
    }

    public function current(): int {
        return $this->values[$this->index] ?? 0;
    }

    public function key(): string {
        return sprintf($this->keyMask, $this->index);
    }

    public function next(): void {
        ++$this->index;
    }

    public function valid(): bool {
        return $this->index < $this->values->count();
    }
}

It does feel like a backward step, and may not be the best solution, but at least it works across all the versions of PHP that I might want to work with, from 7.1 upwards, and only the void return typehints preclude it from working with 7.0.

What this does highlight is how important it is to test your code against PHP 8 if you plan on upgrading your PHP.

This entry was posted in PHP and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s