Programster's Blog

Tutorials focusing on Linux, programming, and open-source

PHP - Creating Strict Type Arrays (Collections)

PHP

PHP doesn't support generics (yet) like in Java where you could create an array of type car by simply running:

Car[] cars = {new Car(), new Car()};

So in PHP we have the following choices.

  1. Stick with using plain arrays and not having the benefits of automatic type validation.
  2. Create a class for each type of strict array you want.
  3. Create and use a generic class for a strict array, but has some tradeoffs from point 2 (explained later).

I will often say "collections" instead of arrays. However, in these examples I am clearly overriding the ArrayObject class and collections are a different interface. When I talk of collections, I just mean a group of objects, but I felt I had to be clear here with saying "Array". At the end of the day, an array is an implementation of a collection, but not all collections are arrays. Just like squares and rectangles.

Car Class

For all of the examples below, you will need a Car object. We don't actually care about its implmenetation, we just need to create the object type so here it is:

final class Car
{
    public function __construct(){}
}

Individual Classes

Here is an example where I create a CarCollection that extends the ArrayObject. This can only take elements of type Car.

final class CarCollection extends \ArrayObject
{
    public function __construct(Car ...$cars)
    {
        parent::__construct($cars);
    }


    public function append(mixed $value) : void
    {
        if ($value instanceof Car)
        {
            parent::append($value);
        }
        else
        {
            throw new Exception("Cannot append non Car to a " . __CLASS__);
        }
    }


    public function offsetSet(mixed $index, mixed $newval) : void 
    {
        if ($newval instanceof Car)
        {
            parent::offsetSet($index, $newval);
        }
        else
        {
            throw new Exception("Cannot add a non Car value to a " . __CLASS__);
        }
    }
}

The following operations should work:

$car1 = new Car();
$car2 = new Car();
$car3 = new Car();
$carsArray = array($car1, $car2, $car3);

$carCollection1 = new CarCollection(Car::class);
$carCollection1[] = $car1;
$carCollection2[] = $car1;
$carCollection3[] = $car1;

$carCollection2 = new CarCollection(...$carsArray);

$carCollection3 = new CarCollection($car1, $car2);
$carCollection3[] = $car3;

You now have a collection that only takes car objects, and you can specify it as a parameter or return type, making your code clearer, safer, and simpler to use.

Generic Collection

You might find it a pain to create a collection for every type you need. You may wish to create a generic collection like below:

<?php 

final class GenericCollection extends ArrayObject
{
    private $m_elementType;

    public function __construct(string $elementType, ...$elements)
    {
        $this->m_elementType = $elementType;

        foreach ($elements as $element)
        {
            if ($element instanceof $elementType === FALSE)
            {
                throw new Exception("Cannot append non " . $this->m_elementType . " to collection");
            }
        }

        parent::__construct($elements);
    }


    public function append(mixed $value) : void
    {
        if ($value instanceof $this->m_elementType)
        {
            parent::append($value);
        }
        else
        {
            throw new Exception("Cannot append non " . $this->m_elementType . " to collection");
        }
    }


    public function offsetSet($index, $newval) 
    {
        if ($newval instanceof $this->m_elementType)
        {
            parent::offsetSet($index, $newval);
        }
        else
        {
            throw new Exception("Cannot append non " . $this->m_elementType . " to collection");
        }
    }
}

You would use it like:

$car1 = new Car();
$car2 = new Car();
$car3 = new Car();
$carsArray = array($car1, $car2, $car3);

$carCollection1 = new GenericCollection(Car::class);
$carCollection1[] = $car1;
$carCollection2[] = $car1;
$carCollection3[] = $car1;

$carCollection2 = new GenericCollection(Car::class, ...$carsArray);

$carCollection3 = new GenericCollection(Car::class, $car1, $car2);
$carCollection3[] = $car3;

However, this has the following disadvantages.

  • There is not really much point in specifying GenericCollection as a parameter or return type as this doesn't ensure that the method is takes/returns elements of a certain type, only that all the elements would be of the same type.
  • There is a performance penalty. I found that my Intel i5-4670 CPU took twice as long to create the generic collection (in PHP 7.0).
# results when using 30,000,000 objects in constructor
Strict collection required time: 2.6606721878052
Generic collection required time: 4.9988670349121

You can run my test script in the appendix to test for yourself.

Conclusion

Creating strict type collections can make your code "self-validate" and easier to understand. However, until PHP get's proper generics, you are probably going to need to create a class for every collection type you desire which is annoying.

If you feel that you have a better solution I'd be very keen to hear from you in the comments.

Appendix

Below is my full test script.

<?php

final class Car
{
    public function __construct(){}
}


final class CarCollection extends ArrayObject
{
    public function __construct(Car ...$cars)
    {
        parent::__construct($cars);
    }


    public function append($value) 
    {
        if ($value instanceof Car)
        {
            parent::append($value);
        }
        else
        {
            throw new Exception("Cannot append non Car to a " . __CLASS__);
        }
    }


    public function offsetSet($index, $newval) 
    {
        if ($newval instanceof Car)
        {
            parent::offsetSet($index, $newval);
        }
        else
        {
            throw new Exception("Cannot add a non Car value to a " . __CLASS__);
        }
    }
}


final class GenericCollection extends ArrayObject
{
    private $m_elementType;

    public function __construct(string $elementType, ...$elements)
    {
        $this->m_elementType = $elementType;

        foreach ($elements as $element)
        {
            if ($element instanceof $elementType === FALSE)
            {
                throw new Exception("Cannot append non " . $this->m_elementType . " to collection");
            }
        }

        parent::__construct($elements);
    }


    public function append($value) 
    {
        if ($value instanceof $this->m_elementType)
        {
            parent::append($value);
        }
        else
        {
            throw new Exception("Cannot append non " . $this->m_elementType . " to collection");
        }
    }


    public function offsetSet($index, $newval) 
    {
        if ($newval instanceof $this->m_elementType)
        {
            parent::offsetSet($index, $newval);
        }
        else
        {
            throw new Exception("Cannot append non " . $this->m_elementType . " to collection");
        }
    }
}



print "Testing car collection..." . PHP_EOL;
$carCollection = new CarCollection();
$car = new Car();
$carCollection[] = $car;
print "successfully added car. \n";

try
{
    print "Trying to add int... \n";
    $carCollection[] = 3;
    print "FAILED - managed to add int successfully. \n";
}
catch (Exception $e)
{
    print "SUCCESS - managed to block adding int successfully. \n";
}

print PHP_EOL ;
print "Testing generic collection..." . PHP_EOL;
$carCollection = new GenericCollection(Car::class);
$car = new Car();
$carCollection[] = $car;
print "successfully added car. \n";

try
{DDFD
    print "Trying to add int... \n";
    $carCollection[] = 3;
    print "FAILED - managed to add int successfully. \n";
}
catch (Exception $e)
{
    print "SUCCESS - managed to block adding int successfully. \n";
}


print "Performing speed test... \n";
$carList = array();
for ($i=0; $i<30000000; $i++)
{
    $carList[] = new Car();
}


$start = microtime(true);
$strictCollection = new CarCollection(...$carList);
$finish = microtime(true);
print "Strict collection required time: " . ($finish - $start) . PHP_EOL;


$start = microtime(true);
$genericCollection = new GenericCollection(Car::class, ...$carList);
$finish = microtime(true);
print "Generic collection required time: " . ($finish - $start) . PHP_EOL;
Last updated: 27th October 2022
First published: 16th August 2018

This blog is created by Stuart Page

I'm a freelance web developer and technology consultant based in Surrey, UK, with over 10 years experience in web development, DevOps, Linux Administration, and IT solutions.

Need support with your infrastructure or web services?

Get in touch