PHP - Creating Strict Type Arrays (Collections)
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.
- Stick with using plain arrays and not having the benefits of automatic type validation.
- Create a class for each type of strict array you want.
- Create and use a generic class for a strict array, but has some tradeoffs from point 2 (explained later).
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;
First published: 16th August 2018