Programster's Blog

Tutorials focusing on Linux, programming, and open-source

PHP - Creating Strict Type Arrays

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($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__);
        }
    }
}

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:


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"); } } }

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;