X
Popular Searches

Approaches to Creating Typed Arrays in PHP

PHP Logo

PHP doesn’t let you define typed arrays. Any array can contain any value, which makes it tricky to enforce consistency in your codebase. Here are a few workarounds to help you create typed collections of objects using existing PHP features.

Identifying the Problem

PHP arrays are a very flexible data structure. You can add whatever you like to an array, ranging from scalar values to complex objects:

$arr = [
    "foobar",
    123,
    new DateTimeImmutable()
];

In practice, it’s rare you’d actually want an array with such a varied range of values. It’s more likely that your arrays will contain multiple instances of the same kind of value.

$times = [
    new DateTimeImmutable(),
    new DateTimeImmutable(),
    new DateTimeImmutable()
];

You might then create a method which acts on all the values within your array:

final class Stopwatch {
 
    protected array $laps = [];
 
    public function recordLaps(array $times) : void {
        foreach ($times as $time) {
            $this -> laps[] = $time -> getTimestamp();
        }
    }
 
}

This code iterates over the DateTimeInterface instances in $times. The Unix timestamp representation of the time (seconds measured as an integer) is then stored into $laps.

The trouble with this code is it makes an assumption that $times is comprised wholly of DateTimeInterface instances. There’s nothing to guarantee this is the case so a caller could still pass an array of mixed values. If one of the values didn’t implement DateTimeInterface, the call to getTimestamp() would be illegal and a runtime error would occur.

$stopwatch = new Stopwatch();
 
// OK
$stopwatch -> recordLaps([
    new DateTimeImmutable(),
    new DateTimeImmutable()
]);
 
// Crash!
$stopwatch -> recordLaps([
    new DateTimeImmutable(),
    123     // can't call `getTimestamp()` on an integer!
]);

Adding Type Consistency with Variadic Arguments

Ideally the issue would be resolved by specifying that the $times array can only contain DateTimeInterface instances. As PHP lacks support for typed arrays, we must look to alternative language features instead.

The first option is to use variadic arguments and unpack the $times array before it’s passed to recordLaps(). Variadic arguments allow a function to accept an unknown number of arguments which are then made available as a single array. Importantly for our use case, you may typehint variadic arguments as normal. Each argument passed in must then be of the given type.

Variadic arguments are commonly used for mathematical functions. Here’s a simple example that sums every argument it’s given:

function sumAll(int ...$numbers) {
    return array_sum($numbers);
}
 
echo sumAll(1, 2, 3, 4, 5);     // emits 15

sumAll() is not passed an array. Instead, it receives multiple arguments which PHP combines into the $numbers array. The int typehint means each value must be an integer. This acts as a guarantee that $numbers will only consist of integers. We can now apply this to the stopwatch example:

final class Stopwatch {
 
    protected array $laps = [];
 
    public function recordLaps(DateTimeInterface ...$times) : void {
        foreach ($times as $time) {
            $this -> laps[] = $time -> getTimestamp();
        }
    }
 
}
 
$stopwatch = new Stopwatch();
 
$stopwatch -> recordLaps(
    new DateTimeImmutable(),
    new DateTimeImmutable()
);

It’s no longer possible to pass unsupported types into recordLaps(). Attempts to do so will be surfaced much earlier, before the getTimestamp() call is attempted.

If you’ve already got an array of times to pass to recordLaps(), you’ll need to unpack it with the splat operator (...) when you call the method. Trying to pass it directly will fail – it’d be treated as one of the variadic times, which are required to be an int and not an array.

$times = [
    new DateTimeImmutable(),
    new DateTimeImmutable()
];
 
$stopwatch -> recordLaps(...$times);

Limitations of Variadic Arguments

Variadic arguments can be a great help when you need to pass an array of items to a function. However, there are some restrictions on how they can be used.

The most significant limitation is that you can only use one set of variadic arguments per function. This means each function can accept only one “typed” array. In addition, the variadic argument must be defined last, after any regular arguments.

function variadic(string $something, DateTimeInterface ...$times);

By nature, variadic arguments can only be used with functions. This means they can’t help you out when you need to store an array as a property, or return it from a function. We can see this in the stopwatch code – the Stopwatch class has a laps array which is meant to store only integer timestamps. There’s currently no way we can enforce this is the case.

Collection Classes

In these circumstances a different approach must be selected. One way to create something close to a “typed array” in userland PHP is to write a dedicated collection class:

final class User {
 
    protected string $Email;
 
    public function getEmail() : string {
        return $this -> Email;
    }
 
}
 
final class UserCollection implements IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}

The UserCollection class can now be used anywhere you’d normally expect an array of User instances. UserCollection uses variadic arguments to accept a series of User instances in its constructor. Although the $Users property has to be typehinted as the generic array, it’s guaranteed to consist wholly of user instances as it’s only written to in the constructor.

It may seem tempting to provide a get() : array method which exposes all the collection’s items. This should be avoided as it brings us back to the vague array typehint problem. Instead, the collection is made iterable so consumers can use it in a foreach loop. In this way, we’ve managed to create a typehint-able “array” which our code can safely assume contains only users.

function sendMailToUsers(UserCollection $Users) : void {
    foreach ($Users as $User) {
        mail($user -> getEmail(), "Test Email", "Hello World!");
    }
}
 
$users = new UserCollection(new User(), new User());
sendMailToUsers($users);

Making Collections More Array-Like

Collection classes solve the typehinting problem but do mean you lose some of the useful functionality of arrays. Built-in PHP functions like count() and isset() won’t work with your custom collection class.

Support for these functions can be added by implementing additional built-in interfaces. If you implement Countable, your class will be usable with count():

final class UserCollection implements Countable, IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function count() : int {
        return count($this -> Users);
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}
 
$users = new UserCollection(new User(), new User());
echo count($users);     // 2

Implementing ArrayAccess lets you access items in your collection using array syntax. It also enables the isset() and unset() functions. You need to implement four methods so PHP can interact with your items.

final class UserCollection implements ArrayAccess, IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function offsetExists(mixed $offset) : bool {
        return isset($this -> Users[$offset]);
    }
 
    public function offsetGet(mixed $offset) : User {
        return $this -> Users[$offset];
    }
 
    public function offsetSet(mixed $offset, mixed $value) : void {
        if ($value instanceof User) {
            $this -> Users[$offset] = $value;
        }
        else throw new \TypeError("Not a user!");
    }
 
    public function offsetUnset(mixed $offset) : void {
        unset($this -> Users[$offset]);
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}
 
$users = new UserCollection(
    new User("example@example.com"),
    new User("hello@world.com")
);
 
echo $users[1] -> getEmail();   // hello@world.com
var_dump(isset($users[2]));     // false

You now have a class which can only contain User instances and which also looks and feels like an array. One point to note about ArrayAccess is the offsetSet implementation – as $value must be mixed, this could allow incompatible values to be added to your collection. We explicitly check the type of the passed $value to prevent this.

Conclusion

Recent PHP releases have evolved the language towards stronger typing and greater consistency. This doesn’t yet extend to array elements though. Typehinting against array is often too relaxed but you can circumvent the limitations by building your own collection classes.

When combined with variadic arguments, the collection pattern is a viable way to enforce the types of aggregate values in your code. You can typehint your collections and iterate over them knowing only one type of value will be present.

James Walker James Walker
James Walker is a contributor to CloudSavvy IT. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience of managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker and Kubernetes. Read Full Bio »

The above article may contain affiliate links, which help support CloudSavvy IT.