The PHP splat operator has been in PHP for a while. Since PHP 5.6. I've discovered it quite recently by browsing PHP docs. From a quick google search it doesn't seem it's being used much. What I tried right away is if it can be used for type hinting each array parameter instead of just using just @param int[] $ints method annotation. Like this:

public function setInts(int ...$ints): void
{
    $this->ints = $ints;
}

When such method would be called it would look like this:

$something->setInts(...[1,2,3]);

That's neat, I tought. I could build quite nice collection classes that would check each type of the input array. And of course I stated working on that as it was one of the ideas I couldn't get out of my head other than implementing it. I also checked github whether someone has not created such library already. And of course, someone has. For example this library was exactly what I had in mind and even had more details that I learned from. Like extending the SplFixedArray which is iterable object of fixed size that uses less memory and it's faster than PHP arrays. Also check other SPL objects if you don't know them already. Or that the author built it immutable and disabled all methods that enabled any mutation. Amazing! I do not understand how come it has only 4 stars.

After 3 evenings of implementing my version of immutable collections into the project I was working on I started thinking whether the conversion from array to a parameter list and back wouldn't slow thing down. So I created a small benchmark.

Benchmark script

<?php

class ArrayCollection
{
    private $ints;

    public function __construct(array $ints)
    {
        $this->ints = $ints;
    }
}

class SplatCollection
{
    private $ints;

    public function __construct(int ...$ints)
    {
        $this->ints = $ints;
    }
}

class MapTypeCheckCollection
{
    private $ints;

    public function __construct(array $ints)
    {
        $this->ints = array_map(function (int $int) {
            return $int;
        }, $ints);
    }
}

class ForeachTypeCheckCollection
{
    private $ints;

    public function __construct(array $ints)
    {
        foreach($ints as $int) {
            if (!is_int($int)) {
                throw new \Exception("{$int} must be type of integer");
            }
        }

        $this->ints = $ints;
    }
}

$input = [];

for ($i = 0; $i < 1000000; $i++) {
    $input[] = $i;
}

$time = microtime(true);
$memory = memory_get_usage(true);

new ArrayCollection($input);

writeResults(ArrayCollection::class, $time, $memory);

$time = microtime(true);
$memory = memory_get_usage(true);

new SplatCollection(...$input);

writeResults(SplatCollection::class, $time, $memory);

$time = microtime(true);
$memory = memory_get_usage(true);

new MapTypeCheckCollection($input);

writeResults(MapTypeCheckCollection::class, $time, $memory);

$time = microtime(true);
$memory = memory_get_usage(true);

new ForeachTypeCheckCollection($input);

writeResults(ForeachTypeCheckCollection::class, $time, $memory);

function writeResults(string $class, float $time, float $memory): void
{
    $timeUsage = microtime(true) - $time;
    $memoryUsage = memory_get_usage(true) - $memory;
    echo "{$class} created in {$timeUsage} s and used {$memoryUsage} \n\n";
}

A quick overview of what's going on there. I created 4 classes that simulate the collections.

  • ArrayCollection - no array element check. It only checks that some array is added. That's how I did it the "old" way.
  • SplatCollection - the collection that checks each array element with the splat operator. That's what I wanted to use.
  • MapTypeCheckCollection - another way how to check type of each array element with array_map function.
  • ForeachTypeCheckCollection - and I threw in also the ugly way of using foreach, checking type of each element and throw exception if the type is wrong.

Then I generate an array of 1.000.000 integers that I pass to each collection class and check how long it takes to instantiate them.

Benchmark results

And here is the output running on my Macbook Pro 2.9 GHz Intel Core i5, 16 GB RAM. It doesn't really matter how fast it is on a specific computer, but rather to see the comparison between the speeds.

$ php splat-performance.php 
ArrayCollection created in 0.00011801719665527 s and used 0 

SplatCollection created in 0.056730031967163 s and used 0 

MapTypeCheckCollection created in 1.2292590141296 s and used 0 

ForeachTypeCheckCollection created in 0.074965953826904 s and used 0 

Here it is as a chart to see the scale of each solution.

benchmark chart

The array_map function is awefully slow here. I would guess it will be similar to the foreach version. From a google search it seems that other benchmarks also spotted that array_map is way slower than foreach. Maybe I'll stop using it altogether. Although I like the functional programming feel that it brings.

EDIT: I made another benchmark for array_map vs foreach

Splat type error message

Just for complete information. If I'd send ['string'] array to the SplatCollection it would throw this nice error message. Too bad it's not an exception that could be caught during runtime.

Fatal error: Uncaught TypeError: Argument 1 passed to SplatCollection::__construct() must be of the type integer, string given, called in splat-performance.php on line 37 and defined in splat-performance.php on line 17

TypeError: Argument 1 passed to SplatCollection::__construct() must be of the type integer, string given, called in splat-performance.php on line 37 in splat-performance.php on line 17

Call Stack:
    0.0004     358776   1. {main}() splat-performance.php:0
    0.0025     358864   2. SplatCollection->__construct() splat-performance.php:37

Conclusion

So I won't use the splat operator for collections because of low speed and the error that cannot be caught on runtime. There was this RFC for PHP 5.6 that proposed a typed array element implementation, but it was not agreed upon. Hopefully it will be in a future PHP version.

I will, however, place the splat operator into my PHP tool set from now on as I have finally discovered it. For example I just used it for string deserialization like

$whateverObject->addTypeAndId(...explode(':', 'TypeA:123'));

This looks quite elegant. And that's one of the use cases what it was meant for I guess.

Next Post Previous Post